From b139a96b45101830bb44e1854c4e96552cdfbc1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Van=20Der=20Linden?= <117538+jeromevdl@users.noreply.github.com> Date: Mon, 11 Dec 2023 15:41:48 +0100 Subject: [PATCH] feat(v2): new logging module (#1435) --- docs/core/logging.md | 1134 ++++++++++++----- docs/stylesheets/extra.css | 2 +- examples/pom.xml | 2 +- examples/powertools-examples-batch/pom.xml | 7 +- .../dynamo/DynamoDBStreamBatchHandler.java | 6 +- .../org/demo/batch/dynamo/DynamoDBWriter.java | 6 +- .../batch/kinesis/KinesisBatchHandler.java | 6 +- .../batch/kinesis/KinesisBatchSender.java | 6 +- .../org/demo/batch/sqs/SqsBatchHandler.java | 6 +- .../org/demo/batch/sqs/SqsBatchSender.java | 6 +- .../pom.xml | 25 +- .../src/main/java/helloworld/App.java | 20 +- .../cdk/app/pom.xml | 4 +- .../cdk/app/src/main/java/helloworld/App.java | 7 +- .../gradle/build.gradle | 3 +- .../gradle/src/main/java/helloworld/App.java | 6 +- .../kotlin/build.gradle.kts | 16 +- .../kotlin/src/main/kotlin/helloworld/App.kt | 5 +- .../src/test/kotlin/helloworld/AppTest.kt | 20 - .../sam/pom.xml | 17 +- .../sam/src/main/java/helloworld/App.java | 6 +- .../serverless/pom.xml | 17 +- .../terraform/pom.xml | 17 +- .../powertools-examples-idempotency/pom.xml | 22 +- .../src/main/java/helloworld/App.java | 6 +- .../powertools-examples-parameters/pom.xml | 6 +- .../demo/parameters/ParametersFunction.java | 6 +- .../powertools-examples-serialization/pom.xml | 4 +- ...GatewayRequestDeserializationFunction.java | 13 +- .../SQSEventDeserializationFunction.java | 6 +- .../powertools-examples-validation/pom.xml | 6 +- pom.xml | 64 +- powertools-batch/pom.xml | 2 +- .../handler/DynamoDbBatchMessageHandler.java | 2 +- .../KinesisStreamsBatchMessageHandler.java | 2 +- .../batch/handler/SqsBatchMessageHandler.java | 4 +- powertools-cloudformation/pom.xml | 2 +- powertools-common/pom.xml | 12 +- .../common/internal/LambdaConstants.java | 6 - .../internal/LambdaHandlerProcessor.java | 1 - .../internal/UserAgentConfigurator.java | 6 +- .../internal/LambdaHandlerProcessorTest.java | 3 - powertools-e2e-tests/handlers/batch/pom.xml | 6 +- .../lambda/powertools/e2e/Function.java | 24 +- .../handlers/idempotency/pom.xml | 6 +- .../handlers/largemessage/pom.xml | 6 +- .../handlers/largemessage_idempotent/pom.xml | 6 +- powertools-e2e-tests/handlers/logging/pom.xml | 11 +- .../lambda/powertools/e2e/Function.java | 8 +- .../handlers/parameters/pom.xml | 2 +- powertools-e2e-tests/handlers/pom.xml | 13 +- powertools-e2e-tests/pom.xml | 3 +- .../amazon/lambda/powertools/LoggingE2ET.java | 4 +- powertools-idempotency/pom.xml | 11 +- .../powertools/idempotency/Idempotency.java | 2 +- .../handlers/IdempotencyFunction.java | 6 +- powertools-large-messages/pom.xml | 7 +- powertools-logging/pom.xml | 55 +- .../powertools-logging-log4j/pom.xml | 137 ++ .../json/resolver/PowertoolsResolver.java | 286 +++++ .../resolver}/PowertoolsResolverFactory.java | 15 +- .../log4/internal/Log4jLoggingManager.java | 48 + .../src/main/resources/LambdaEcsLayout.json | 89 ++ .../src/main/resources/LambdaJsonLayout.json | 72 ++ ...powertools.logging.internal.LoggingManager | 1 + .../PowerToolsResolverFactoryTest.java | 95 ++ .../PowertoolsMessageResolverTest.java | 115 ++ .../json/resolver/PowertoolsResolverTest.java | 110 ++ .../internal/Log4jLoggingManagerTest.java | 51 + .../handler/PowertoolsJsonMessage.java | 47 + .../handler/PowertoolsLogEnabled.java | 34 + .../test/resources/junit-platform.properties | 17 + .../src/test/resources/log4j2.xml | 24 + .../powertools-logging-logback/pom.xml | 137 ++ .../logging/logback/LambdaEcsEncoder.java | 199 +++ .../logging/logback/LambdaJsonEncoder.java | 208 +++ .../logging/logback/internal/JsonUtils.java | 130 ++ .../logback/internal/LambdaEcsSerializer.java | 187 +++ .../internal/LambdaJsonSerializer.java | 150 +++ .../internal/LogbackLoggingManager.java | 59 + ...powertools.logging.internal.LoggingManager | 1 + .../logging/LogbackLoggingManagerTest.java | 54 + .../internal/LambdaEcsEncoderTest.java | 176 +++ .../internal/LambdaJsonEncoderTest.java | 263 ++++ .../handler/PowertoolsJsonMessage.java | 44 + .../handler/PowertoolsLogEnabled.java | 34 + .../test/resources/junit-platform.properties | 17 + .../src/test/resources/logback-test.xml | 27 + powertools-logging/spotbugs-exclude.xml | 30 + ...Constants.java => CorrelationIdPaths.java} | 14 +- .../lambda/powertools/logging/Logging.java | 37 +- .../powertools/logging/LoggingUtils.java | 56 +- .../internal/AbstractJacksonLayoutCopy.java | 519 -------- .../internal/DefautlLoggingManager.java | 35 + .../logging/internal/JacksonFactoryCopy.java | 133 -- .../logging/internal/LambdaJsonLayout.java | 246 ---- .../logging/internal/LambdaLoggingAspect.java | 372 ++++-- .../internal/LambdaTimestampResolver.java | 169 --- .../LambdaTimestampResolverFactory.java | 49 - .../logging/internal/LoggingConstants.java | 14 +- .../logging/internal/LoggingManager.java | 48 + ...ields.java => PowertoolsLoggedFields.java} | 32 +- .../logging/internal/PowertoolsResolver.java | 66 - .../src/main/resources/LambdaEcsLayout.json | 52 - .../src/main/resources/LambdaJsonLayout.json | 89 -- .../resources/log4j2.component.properties | 2 - .../core/layout/LambdaJsonLayoutTest.java | 173 --- .../java/org/slf4j/test/OutputChoice.java | 77 ++ .../test/java/org/slf4j/test/TestLogger.java | 452 +++++++ .../slf4j/test/TestLoggerConfiguration.java | 204 +++ .../org/slf4j/test/TestLoggerFactory.java | 78 ++ .../org/slf4j/test/TestServiceProvider.java | 76 ++ .../powertools/logging/LoggingUtilsTest.java | 70 +- ...erToolLogEventEnabledWithCustomMapper.java | 63 - .../PowertoolsLogAlbCorrelationId.java | 8 +- ...olsLogApiGatewayHttpApiCorrelationId.java} | 10 +- ...olsLogApiGatewayRestApiCorrelationId.java} | 10 +- .../PowertoolsLogAppSyncCorrelationId.java | 37 + ...tate.java => PowertoolsLogClearState.java} | 17 +- ...sabled.java => PowertoolsLogDisabled.java} | 2 +- ...va => PowertoolsLogDisabledForStream.java} | 2 +- .../handlers/PowertoolsLogEnabled.java | 52 + ...ava => PowertoolsLogEnabledForStream.java} | 2 +- .../logging/handlers/PowertoolsLogError.java | 28 + ...ntEnabled.java => PowertoolsLogEvent.java} | 4 +- ...PowertoolsLogEventBridgeCorrelationId.java | 8 +- ...d.java => PowertoolsLogEventDisabled.java} | 4 +- ....java => PowertoolsLogEventForStream.java} | 8 +- .../handlers/PowertoolsLogResponse.java | 28 + .../PowertoolsLogResponseForStream.java | 35 + ...ava => PowertoolsLogSamplingDisabled.java} | 19 +- ...java => PowertoolsLogSamplingEnabled.java} | 12 +- .../internal/LambdaLoggingAspectTest.java | 709 ++++++++--- .../logging/internal/TestLoggingManager.java | 32 + .../org.slf4j.spi.SLF4JServiceProvider | 1 + ...powertools.logging.internal.LoggingManager | 15 + .../src/test/resources/log4j2.xml | 16 - .../test/resources/s3EventNotification.json | 38 - .../src/test/resources/testlogger.properties | 3 + powertools-metrics/pom.xml | 7 +- powertools-parameters/pom.xml | 7 +- powertools-serialization/pom.xml | 6 +- .../powertools/utilities/JsonConfig.java | 2 +- powertools-tracing/pom.xml | 7 +- powertools-validation/pom.xml | 2 +- .../validation/ValidationConfig.java | 2 +- spotbugs-exclude.xml | 45 +- 147 files changed, 6070 insertions(+), 2778 deletions(-) delete mode 100644 examples/powertools-examples-core-utilities/kotlin/src/test/kotlin/helloworld/AppTest.kt create mode 100644 powertools-logging/powertools-logging-log4j/pom.xml create mode 100644 powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolver.java rename powertools-logging/{src/main/java/software/amazon/lambda/powertools/logging/internal => powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver}/PowertoolsResolverFactory.java (71%) create mode 100644 powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4/internal/Log4jLoggingManager.java create mode 100644 powertools-logging/powertools-logging-log4j/src/main/resources/LambdaEcsLayout.json create mode 100644 powertools-logging/powertools-logging-log4j/src/main/resources/LambdaJsonLayout.json create mode 100644 powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager create mode 100644 powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowerToolsResolverFactoryTest.java create mode 100644 powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsMessageResolverTest.java create mode 100644 powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolverTest.java create mode 100644 powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/Log4jLoggingManagerTest.java create mode 100644 powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsJsonMessage.java create mode 100644 powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEnabled.java create mode 100644 powertools-logging/powertools-logging-log4j/src/test/resources/junit-platform.properties create mode 100644 powertools-logging/powertools-logging-log4j/src/test/resources/log4j2.xml create mode 100644 powertools-logging/powertools-logging-logback/pom.xml create mode 100644 powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaEcsEncoder.java create mode 100644 powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaJsonEncoder.java create mode 100644 powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/JsonUtils.java create mode 100644 powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/LambdaEcsSerializer.java create mode 100644 powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/LambdaJsonSerializer.java create mode 100644 powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/LogbackLoggingManager.java create mode 100644 powertools-logging/powertools-logging-logback/src/main/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager create mode 100644 powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java create mode 100644 powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaEcsEncoderTest.java create mode 100644 powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonEncoderTest.java create mode 100644 powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsJsonMessage.java create mode 100644 powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEnabled.java create mode 100644 powertools-logging/powertools-logging-logback/src/test/resources/junit-platform.properties create mode 100644 powertools-logging/powertools-logging-logback/src/test/resources/logback-test.xml create mode 100644 powertools-logging/spotbugs-exclude.xml rename powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/{CorrelationIdPathConstants.java => CorrelationIdPaths.java} (69%) delete mode 100644 powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/AbstractJacksonLayoutCopy.java create mode 100644 powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/DefautlLoggingManager.java delete mode 100644 powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/JacksonFactoryCopy.java delete mode 100644 powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonLayout.java delete mode 100644 powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaTimestampResolver.java delete mode 100644 powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaTimestampResolverFactory.java create mode 100644 powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManager.java rename powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/{DefaultLambdaFields.java => PowertoolsLoggedFields.java} (59%) delete mode 100644 powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsResolver.java delete mode 100644 powertools-logging/src/main/resources/LambdaEcsLayout.json delete mode 100644 powertools-logging/src/main/resources/LambdaJsonLayout.json delete mode 100644 powertools-logging/src/main/resources/log4j2.component.properties delete mode 100644 powertools-logging/src/test/java/org/apache/logging/log4j/core/layout/LambdaJsonLayoutTest.java create mode 100644 powertools-logging/src/test/java/org/slf4j/test/OutputChoice.java create mode 100644 powertools-logging/src/test/java/org/slf4j/test/TestLogger.java create mode 100644 powertools-logging/src/test/java/org/slf4j/test/TestLoggerConfiguration.java create mode 100644 powertools-logging/src/test/java/org/slf4j/test/TestLoggerFactory.java create mode 100644 powertools-logging/src/test/java/org/slf4j/test/TestServiceProvider.java delete mode 100644 powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabledWithCustomMapper.java rename powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/{PowerLogToolApiGatewayHttpApiCorrelationId.java => PowertoolsLogApiGatewayHttpApiCorrelationId.java} (78%) rename powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/{PowerLogToolApiGatewayRestApiCorrelationId.java => PowertoolsLogApiGatewayRestApiCorrelationId.java} (78%) create mode 100644 powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogAppSyncCorrelationId.java rename powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/{PowertoolsLogEnabledWithClearState.java => PowertoolsLogClearState.java} (67%) rename powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/{PowerToolDisabled.java => PowertoolsLogDisabled.java} (91%) rename powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/{PowerToolDisabledForStream.java => PowertoolsLogDisabledForStream.java} (92%) create mode 100644 powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabled.java rename powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/{PowerLogToolEnabledForStream.java => PowertoolsLogEnabledForStream.java} (93%) create mode 100644 powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogError.java rename powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/{PowerToolLogEventEnabled.java => PowertoolsLogEvent.java} (92%) rename powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/{PowerToolLogEventDisabled.java => PowertoolsLogEventDisabled.java} (89%) rename powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/{PowerToolLogEventEnabledForStream.java => PowertoolsLogEventForStream.java} (80%) create mode 100644 powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogResponse.java create mode 100644 powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogResponseForStream.java rename powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/{PowerLogToolEnabled.java => PowertoolsLogSamplingDisabled.java} (68%) rename powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/{PowerLogToolSamplingEnabled.java => PowertoolsLogSamplingEnabled.java} (74%) create mode 100644 powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/TestLoggingManager.java create mode 100644 powertools-logging/src/test/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider create mode 100644 powertools-logging/src/test/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager delete mode 100644 powertools-logging/src/test/resources/log4j2.xml delete mode 100644 powertools-logging/src/test/resources/s3EventNotification.json create mode 100644 powertools-logging/src/test/resources/testlogger.properties diff --git a/docs/core/logging.md b/docs/core/logging.md index 70781b1b2..f0fba760a 100644 --- a/docs/core/logging.md +++ b/docs/core/logging.md @@ -5,24 +5,34 @@ description: Core utility Logging provides an opinionated logger with output structured as JSON. -**Key features** +## Key features -* Capture key fields from Lambda context, cold start and structures logging output as JSON -* Log Lambda event when instructed, disabled by default, can be enabled explicitly via annotation param -* Append additional keys to structured log at any point in time +* Leverages standard logging libraries: [_SLF4J_](https://www.slf4j.org/){target="_blank"} as the API, and [_log4j2_](https://logging.apache.org/log4j/2.x/){target="_blank"} or [_logback_](https://logback.qos.ch/){target="_blank"} for the implementation +* Captures key fields from Lambda context, cold start and structures logging output as JSON +* Optionally logs Lambda request +* Optionally logs Lambda response +* Optionally supports log sampling by including a configurable percentage of DEBUG logs in logging output +* Allows additional keys to be appended to the structured log at any point in time -## Install -Depending on your version of Java (either Java 1.8 or 11+), the configuration slightly changes. +## Getting started -=== "Maven Java 11+" +???+ tip + You can find complete examples in the [project repository](https://github.com/aws-powertools/powertools-lambda-java/tree/v2/examples/powertools-examples-core-utilities){target="_blank"}. - ```xml hl_lines="3-7 16 18 24-27" +### Installation +Depending on preference, you must choose to use either _log4j2_ or _logback_ as your log provider. In both cases you need to configure _aspectj_ +to weave the code and make sure the annotation is processed. + +#### Maven +=== "log4j2" + + ```xml hl_lines="3-7 24-27" ... software.amazon.lambda - powertools-logging + powertools-logging-log4j {{ powertools.version }} ... @@ -60,14 +70,14 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl ``` -=== "Maven Java 1.8" +=== "logback" - ```xml hl_lines="3-7 16 18 24-27" + ```xml hl_lines="3-7 24-27" ... software.amazon.lambda - powertools-logging + powertools-logging-logback {{ powertools.version }} ... @@ -78,13 +88,13 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl ... - org.codehaus.mojo + dev.aspectj aspectj-maven-plugin - 1.14.0 + 1.13.1 - 1.8 - 1.8 - 1.8 + 11 + 11 + 11 software.amazon.lambda @@ -105,7 +115,9 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl ``` -=== "Gradle Java 11+" +#### Gradle + +=== "log4j2" ```groovy hl_lines="3 11" plugins { @@ -118,19 +130,19 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl } dependencies { - aspect 'software.amazon.lambda:powertools-logging:{{ powertools.version }}' + aspect 'software.amazon.lambda:powertools-logging-log4j:{{ powertools.version }}' } sourceCompatibility = 11 targetCompatibility = 11 ``` -=== "Gradle Java 1.8" +=== "logback" ```groovy hl_lines="3 11" plugins { id 'java' - id 'io.freefair.aspectj.post-compile-weaving' version '6.6.3' + id 'io.freefair.aspectj.post-compile-weaving' version '8.1.0' } repositories { @@ -138,27 +150,61 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl } dependencies { - aspect 'software.amazon.lambda:powertools-logging:{{ powertools.version }}' + aspect 'software.amazon.lambda:powertools-logging-logback:{{ powertools.version }}' } - sourceCompatibility = 1.8 - targetCompatibility = 1.8 + sourceCompatibility = 11 + targetCompatibility = 11 ``` -## Initialization +### Configuration -Powertools for AWS Lambda (Java) extends the functionality of Log4J. Below is an example `#!xml log4j2.xml` file, with the `JsonTemplateLayout` using `#!json LambdaJsonLayout.json` configured. +#### Main environment variables -!!! info "LambdaJsonLayout is now deprecated" +The logging module requires two settings: - Configuring utiltiy using `` plugin is deprecated now. While utility still supports the old configuration, we strongly recommend upgrading the - `log4j2.xml` configuration to `JsonTemplateLayout` instead. [JsonTemplateLayout](https://logging.apache.org/log4j/2.x/manual/json-template-layout.html) is recommended way of doing structured logging. - - Please follow [this guide](#upgrade-to-jsontemplatelayout-from-deprecated-lambdajsonlayout-configuration-in-log4j2xml) for upgrade steps. +| Environment variable | Setting | Description | +|---------------------------|-------------------|-------------------------------------------------------------------------------------------------------------| +| `POWERTOOLS_LOG_LEVEL` | **Logging level** | Sets how verbose Logger should be. If not set, will use the [Logging configuration](#logging-configuration) | +| `POWERTOOLS_SERVICE_NAME` | **Service** | Sets service key that will be included in all log statements (Default is `service_undefined`) | + +Here is an example using AWS Serverless Application Model (SAM): + +=== "template.yaml" +``` yaml hl_lines="10 11" +Resources: + PaymentFunction: + Type: AWS::Serverless::Function + Properties: + MemorySize: 512 + Timeout: 20 + Runtime: java17 + Environment: + Variables: + POWERTOOLS_LOG_LEVEL: WARN + POWERTOOLS_SERVICE_NAME: payment +``` + +There are some other environment variables which can be set to modify Logging's settings at a global scope: + +| Environment variable | Type | Description | +|---------------------------------|----------|-------------------------------------------------------------------------------------------------------------------------| +| `POWERTOOLS_LOGGER_SAMPLE_RATE` | float | Configure the sampling rate at which `DEBUG` logs should be included. See [sampling rate](#sampling-debug-logs) | +| `POWERTOOLS_LOG_EVENT` | boolean | Specify if the incoming Lambda event should be logged. See [Logging event](#logging-incoming-event) | +| `POWERTOOLS_LOG_RESPONSE` | boolean | Specify if the Lambda response should be logged. See [logging response](#logging-handler-response) | +| `POWERTOOLS_LOG_ERROR` | boolean | Specify if a Lambda uncaught exception should be logged. See [logging exception](#logging-handler-uncaught-exception ) | + +#### Logging configuration + +Powertools for AWS Lambda (Java) simply extends the functionality of the underlying library you choose (_log4j2_ or _logback_). +You can leverage the standard configuration files (_log4j2.xml_ or _logback.xml_): === "log4j2.xml" + With log4j2, we leverage the [`JsonTemplateLayout`](https://logging.apache.org/log4j/2.x/manual/json-template-layout.html){target="_blank"} + to provide structured logging. A default template is provided in powertools ([_LambdaJsonLayout.json_](https://github.com/aws-powertools/powertools-lambda-java/tree/v2/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaJsonLayout.json){target="_blank"}): + ```xml hl_lines="5" @@ -168,7 +214,7 @@ Powertools for AWS Lambda (Java) extends the functionality of Log4J. Below is an - + @@ -178,152 +224,225 @@ Powertools for AWS Lambda (Java) extends the functionality of Log4J. Below is an ``` -You can also override log level by setting **`POWERTOOLS_LOG_LEVEL`** env var. Here is an example using AWS Serverless Application Model (SAM) +=== "logback.xml" -=== "template.yaml" - ``` yaml hl_lines="9 10" - Resources: - HelloWorldFunction: - Type: AWS::Serverless::Function - Properties: - ... - Runtime: java8 - Environment: - Variables: - POWERTOOLS_LOG_LEVEL: DEBUG - POWERTOOLS_SERVICE_NAME: example + With logback, we leverage a custom [Encoder](https://logback.qos.ch/manual/encoders.html){target="_blank"} + to provide structured logging: + + ```xml hl_lines="4 5" + + + + + + + + + + + + + ``` -You can also explicitly set a service name via **`POWERTOOLS_SERVICE_NAME`** env var. This sets **service** key that will be present across all log statements. +## Log level +Log level is generally configured in the `log4j2.xml` or `logback.xml`. But this level is static and needs a redeployment of the function to be changed. +Powertools for AWS Lambda permits to change this level dynamically thanks to an environment variable `POWERTOOLS_LOG_LEVEL`. -## Standard structured keys +We support the following log levels (SLF4J levels): `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`. +If the level is set to `CRITICAL` (supported in log4j but not logback), we revert it back to `ERROR`. +If the level is set to any other value, we set it to the default value (`INFO`). -Your logs will always include the following keys to your structured logging: +### AWS Lambda Advanced Logging Controls (ALC) -Key | Type | Example | Description -------------------------------------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------- -**timestamp** | String | "2020-05-24 18:17:33,774" | Timestamp of actual log statement -**level** | String | "INFO" | Logging level -**coldStart** | Boolean | true| ColdStart value. -**service** | String | "payment" | Service name defined. "service_undefined" will be used if unknown -**samplingRate** | int | 0.1 | Debug logging sampling rate in percentage e.g. 10% in this case -**message** | String | "Collecting payment" | Log statement value. Unserializable JSON values will be casted to string -**functionName**| String | "example-powertools-HelloWorldFunction-1P1Z6B39FLU73" -**functionVersion**| String | "12" -**functionMemorySize**| String | "128" -**functionArn**| String | "arn:aws:lambda:eu-west-1:012345678910:function:example-powertools-HelloWorldFunction-1P1Z6B39FLU73" -**xray_trace_id**| String | "1-5759e988-bd862e3fe1be46a994272793" | X-Ray Trace ID when Lambda function has enabled Tracing -**function_request_id**| String | "899856cb-83d1-40d7-8611-9e78f15f32f4"" | AWS Request ID from lambda context +!!!question "When is it useful?" + When you want to set a logging policy to drop informational or verbose logs for one or all AWS Lambda functions, regardless of runtime and logger used. -## Capturing context Lambda info + +With [AWS Lambda Advanced Logging Controls (ALC)](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-advanced){target="_blank"}, you can enforce a minimum log level that Lambda will accept from your application code. -When debugging in non-production environments, you can instruct Logger to log the incoming event with `@Logger(logEvent = true)` or via `POWERTOOLS_LOGGER_LOG_EVENT=true` environment variable. +When enabled, you should keep Powertools and ALC log level in sync to avoid data loss. -!!! warning - Log event is disabled by default to prevent sensitive info being logged. +Here's a sequence diagram to demonstrate how ALC will drop both `INFO` and `DEBUG` logs emitted from `Logger`, when ALC log level is stricter than `Logger`. + +```mermaid +sequenceDiagram + participant Lambda service + participant Lambda function + participant Application Logger -=== "App.java" + Note over Lambda service: AWS_LAMBDA_LOG_LEVEL="WARN" + Note over Application Logger: POWERTOOLS_LOG_LEVEL="DEBUG" - ```java hl_lines="14" - import org.apache.logging.log4j.LogManager; - import org.apache.logging.log4j.Logger; - import software.amazon.lambda.powertools.logging.LoggingUtils; + Lambda service->>Lambda function: Invoke (event) + Lambda function->>Lambda function: Calls handler + Lambda function->>Application Logger: logger.error("Something happened") + Lambda function-->>Application Logger: logger.debug("Something happened") + Lambda function-->>Application Logger: logger.info("Something happened") + Lambda service--xLambda service: DROP INFO and DEBUG logs + Lambda service->>CloudWatch Logs: Ingest error logs +``` + +### Priority of log level settings in Powertools for AWS Lambda + +We prioritise log level settings in this order: + +1. `AWS_LAMBDA_LOG_LEVEL` environment variable +2. `POWERTOOLS_LOG_LEVEL` environment variable +3. level defined in the `log4j2.xml` or `logback.xml` files + +If you set Powertools level lower than ALC, we will emit a warning informing you that your messages will be discarded by Lambda. + +> **NOTE** +> +> With ALC enabled, we are unable to increase the minimum log level below the `AWS_LAMBDA_LOG_LEVEL` environment variable value, see [AWS Lambda service documentation](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-log-level){target="_blank"} for more details. + +## Basic Usage + +To use Lambda Powertools for AWS Lambda Logging, use the `@Logging` annotation in your code and the standard _SLF4J_ logger: + +=== "PaymentFunction.java" + + ```java hl_lines="8 10 12 14" + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.logging.Logging; - ... - - /** - * Handler for requests to Lambda function. - */ - public class App implements RequestHandler { - - Logger log = LogManager.getLogger(App.class); + // ... 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) { - ... + LOGGER.info("Collecting payment"); + // ... + LOGGER.debug("order={}, amount={}", order.getId(), order.getAmount()); + // ... } } ``` -=== "AppLogEvent.java" - - ```java hl_lines="8" - /** - * Handler for requests to Lambda function. - */ - public class AppLogEvent implements RequestHandler { +## Standard structured keys + +Your logs will always include the following keys in your structured logging: + +| Key | Type | Example | Description | +|-------------------|--------|-----------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------| +| **timestamp** | String | "2023-12-01T14:49:19.293Z" | Timestamp of actual log statement, by default uses default AWS Lambda timezone (UTC) | +| **level** | String | "INFO" | Logging level (any level supported by _SLF4J_ (i.e. `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`) | +| **service** | String | "payment" | Service name defined, by default `service_undefined` | +| **sampling_rate** | float | 0.1 | Debug logging sampling rate in percentage e.g. 10% in this case (logged if not 0) | +| **message** | String | "Collecting payment" | Log statement value. Unserializable JSON values will be casted to string | +| **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 { - Logger log = LogManager.getLogger(AppLogEvent.class); + private static final Logger LOGGER = LoggerFactory.getLogger(PaymentFunction.class); - @Logging(logEvent = true) + @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); + + // ... } } ``` -### Customising fields in logs +=== "Order.java" -- Utility by default emits `timestamp` field in the logs in format `yyyy-MM-dd'T'HH:mm:ss.SSSZz` and in system default timezone. -If you need to customize format and timezone, you can do so by configuring `log4j2.component.properties` and configuring properties as shown in example below: + ```java + public class Order { + private String id; + private Date date; + private Double amount; + } + ``` -=== "log4j2.component.properties" +=== "Example CloudWatch Logs" - ```properties hl_lines="1 2" - log4j.layout.jsonTemplate.timestampFormatPattern=yyyy-MM-dd'T'HH:mm:ss.SSSZz - log4j.layout.jsonTemplate.timeZone=Europe/Oslo + ```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", + } ``` -- Utility also provides sample template for [Elastic Common Schema(ECS)](https://www.elastic.co/guide/en/ecs/current/ecs-reference.html) layout. -The field emitted in logs will follow specs from [ECS](https://www.elastic.co/guide/en/ecs/current/ecs-reference.html) together with field captured by utility as mentioned [above](#standard-structured-keys). +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)). - Use `LambdaEcsLayout.json` as `eventTemplateUri` when configuring `JsonTemplateLayout`. +## Additional structured keys -=== "log4j2.xml" +### Logging Lambda context information +The following keys will also be added to all your structured logs (unless [configured otherwise](#more-customization_1)): - ```xml hl_lines="5" - - - - - - - - - - - - - - - - - ``` +| Key | Type | Example | Description | +|--------------------------|---------|----------------------------------------------------------------------------------------|------------------------------------| +| **cold_start** | Boolean | false | ColdStart value | +| **function_name** | String | "example-PaymentFunction-1P1Z6B39FLU73" | Name of the function | +| **function_version** | String | "12" | Version of the function | +| **function_memory_size** | String | "512" | Memory configure for the function | +| **function_arn** | String | "arn:aws:lambda:eu-west-1:012345678910:function:example-PaymentFunction-1P1Z6B39FLU73" | ARN of the function | +| **function_request_id** | String | "899856cb-83d1-40d7-8611-9e78f15f32f4"" | AWS Request ID from lambda context | -## Setting a Correlation ID +### Logging additional keys -You can set a Correlation ID using `correlationIdPath` attribute by passing a [JSON Pointer expression](https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-json-pointer-03){target="_blank"}. +#### Logging a correlation ID -=== "App.java" +You can set a correlation ID using the `correlationIdPath` attribute of the `@Logging`annotation, +by passing a [JMESPath expression](https://jmespath.org/tutorial.html){target="_blank"}, +including our custom [JMESPath Functions](../utilities/serialization.md#built-in-functions). - ```java hl_lines="8" - /** - * Handler for requests to Lambda function. - */ - public class App implements RequestHandler { +=== "AppCorrelationIdPath.java" + + ```java hl_lines="5" + public class AppCorrelationIdPath implements RequestHandler { - Logger log = LogManager.getLogger(App.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AppCorrelationIdPath.class); - @Logging(correlationIdPath = "/headers/my_request_id_header") + @Logging(correlationIdPath = "headers.my_request_id_header") public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { - ... - log.info("Collecting payment") - ... + // ... + LOGGER.info("Collecting payment") + // ... } } ``` -=== "Example Event" +=== "Example HTTP Event" ```json hl_lines="3" { @@ -333,42 +452,89 @@ You can set a Correlation ID using `correlationIdPath` attribute by passing a [J } ``` -=== "Example CloudWatch Logs excerpt" +=== "CloudWatch Logs" - ```json hl_lines="11" + ```json hl_lines="6" { "level": "INFO", "message": "Collecting payment", - "timestamp": "2021-05-03 11:47:12,494+0200", + "timestamp": "2023-12-01T14:49:19.293Z", "service": "payment", - "coldStart": true, - "functionName": "test", - "functionMemorySize": 128, - "functionArn": "arn:aws:lambda:eu-west-1:12345678910:function:test", - "function_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72", "correlation_id": "correlation_id_value" } ``` -We provide [built-in JSON Pointer expression](https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-json-pointer-03){target="_blank"} -for known event sources, where either a request ID or X-Ray Trace ID are present. -=== "App.java" +**setCorrelationId method** - ```java hl_lines="10" - import software.amazon.lambda.powertools.logging.CorrelationIdPathConstants; +You can also use `LoggingUtils.setCorrelationId()` method to inject it anywhere else in your code. - /** - * Handler for requests to Lambda function. - */ - public class App implements RequestHandler { +=== "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** + +To ease routine tasks like extracting correlation ID from popular event sources, +we provide [built-in JMESPath expressions](#built-in-correlation-id-expressions). + +=== "AppCorrelationId.java" + + ```java hl_lines="1 7" + import software.amazon.lambda.powertools.logging.CorrelationIdPaths; + + public class AppCorrelationId implements RequestHandler { - Logger log = LogManager.getLogger(App.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AppCorrelationId.class); - @Logging(correlationIdPath = CorrelationIdPathConstants.API_GATEWAY_REST) + @Logging(correlationIdPath = CorrelationIdPaths.API_GATEWAY_REST) public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { - ... - log.info("Collecting payment") - ... + // ... + LOGGER.info("Collecting payment") + // ... } } ``` @@ -377,316 +543,581 @@ for known event sources, where either a request ID or X-Ray Trace ID are present ```json hl_lines="3" { - "requestContext": { - "requestId": "correlation_id_value" - } + "requestContext": { + "requestId": "correlation_id_value" + } } ``` -=== "Example CloudWatch Logs excerpt" +=== "Example CloudWatch Logs" - ```json hl_lines="11" + ```json hl_lines="6" { "level": "INFO", "message": "Collecting payment", - "timestamp": "2021-05-03 11:47:12,494+0200", + "timestamp": "2023-12-01T14:49:19.293Z", "service": "payment", - "coldStart": true, - "functionName": "test", - "functionMemorySize": 128, - "functionArn": "arn:aws:lambda:eu-west-1:12345678910:function:test", - "function_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72", "correlation_id": "correlation_id_value" } ``` - -## Appending additional keys -!!! info "Custom keys are persisted across warm invocations" - Always set additional keys as part of your handler to ensure they have the latest value, or explicitly clear them with [`clearState=true`](#clearing-all-state). +#### Custom keys -You can append your own keys to your existing logs via `appendKey`. +???+ 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). -=== "App.java" +To append an additional key in your logs, you can use the `LoggingUtils.appendKey()` or `LoggingUtils.appendKeys()` for multiple keys: - ```java hl_lines="11 19" - /** - * Handler for requests to Lambda function. - */ - public class App implements RequestHandler { +=== "PaymentFunction.java" + + ```java hl_lines="8 9 15 16" + public class PaymentFunction implements RequestHandler { - Logger log = LogManager.getLogger(App.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AppLogResponse.class); - @Logging(logEvent = true) + @Logging public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { - ... - LoggingUtils.appendKey("test", "willBeLogged"); - ... - - ... - Map customKeys = new HashMap<>(); - customKeys.put("test", "value"); - customKeys.put("test1", "value1"); + // ... + LoggingUtils.appendKey("orderId", order.getId()); + LOGGER.info("Collecting payment"); - LoggingUtils.appendKeys(customKeys); - ... + // ... + Map customKeys = new HashMap<>(); + customKeys.put("paymentId", payment.getId()); + customKeys.put("amount", payment.getAmount); + LoggingUtils.appendKeys(customKeys); + LOGGER.info("Payment successful"); } } ``` +=== "Example CloudWatch Logs" + + ```json hl_lines="7 16-18" + { + "level": "INFO", + "message": "Collecting payment", + "service": "payment", + "timestamp": "2023-12-01T14:49:19.293Z", + "xray_trace_id": "1-6569f266-4b0c7f97280dcd8428d3c9b5", + "orderId": "41376" + } + ... + { + "level": "INFO", + "message": "Payment successful", + "service": "payment", + "timestamp": "2023-12-01T14:49:20.118Z", + "xray_trace_id": "1-6569f266-4b0c7f97280dcd8428d3c9b5", + "orderId": "41376", + "paymentId": "3245", + "amount": 345.99 + } + ``` + +???+ 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")`. + ### Removing additional keys -You can remove any additional key from entry using `LoggingUtils.removeKeys()`. +You can remove any additional key from entry using `LoggingUtils.removeKey()` or `LoggingUtils.removeKeys()` for multiple keys: -=== "App.java" +=== "PaymentFunction.java" ```java hl_lines="19 20" - /** - * Handler for requests to Lambda function. - */ - public class App implements RequestHandler { + public class PaymentFunction implements RequestHandler { - Logger log = LogManager.getLogger(App.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AppLogResponse.class); - @Logging(logEvent = true) + @Logging(logResponse = true) public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { - ... - LoggingUtils.appendKey("test", "willBeLogged"); - ... - Map customKeys = new HashMap<>(); - customKeys.put("test1", "value"); - customKeys.put("test2", "value1"); + // ... + 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); - ... - LoggingUtils.removeKey("test"); - LoggingUtils.removeKeys("test1", "test2"); - ... + LOGGER.info("Payment successful"); + + // ... + LoggingUtils.removeKey("orderId"); + LoggingUtils.removeKeys("paymentId", "amount"); + + return response; } } ``` -### Clearing all state +=== "Example CloudWatch Logs" + Response is logged (`logResponse=true`) without the additional keys: + + ```json + ... + { + "level": "INFO", + "message": { + "statusCode": 200, + "isBase64Encoded": false, + "body": ..., + "headers": ..., + "multiValueHeaders": ... + }, + "service": "payment", + "timestamp": "2023-12-01T14:49:20.118Z", + "xray_trace_id": "1-6569f266-4b0c7f97280dcd8428d3c9b5" + } + ``` + +???+ tip "Additional keys are based on the MDC" + `LoggingUtils.removeKey("key")` is equivalent to `MDC.remove("key")`. -Logger is commonly initialized in the global scope. Due to [Lambda Execution Context reuse](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-context.html), -this means that custom keys can be persisted across invocations. If you want all custom keys to be deleted, you can use -`clearState=true` attribute on `@Logging` annotation. +#### 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 +`clearState=true` attribute on the `@Logging` annotation. -=== "App.java" +=== "CreditCardFunction.java" - ```java hl_lines="8 12" - /** - * Handler for requests to Lambda function. - */ - public class App implements RequestHandler { + ```java hl_lines="5 8" + public class CreditCardFunction implements RequestHandler { - Logger log = LogManager.getLogger(App.class); + private static final Logger LOGGER = LoggerFactory.getLogger(CreditCardFunction.class); @Logging(clearState = true) public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { - ... - if(input.getHeaders().get("someSpecialHeader")) { - LoggingUtils.appendKey("specialKey", "value"); - } - - log.info("Collecting payment"); - ... + // ... + LoggingUtils.appendKey("cardNumber", card.getId()); + LOGGER.info("Updating card information"); + // ... } } ``` + === "#1 Request" - ```json hl_lines="11" - { - "level": "INFO", - "message": "Collecting payment", - "timestamp": "2021-05-03 11:47:12,494+0200", - "service": "payment", - "coldStart": true, - "functionName": "test", - "functionMemorySize": 128, - "functionArn": "arn:aws:lambda:eu-west-1:12345678910:function:test", - "function_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72", - "specialKey": "value" - } + ```json hl_lines="7" + { + "level": "INFO", + "message": "Updating card information", + "service": "card", + "timestamp": "2023-12-01T14:49:19.293Z", + "xray_trace_id": "1-6569f266-4b0c7f97280dcd8428d3c9b5", + "cardNumber": "6818 8419 9395 5322" + } ``` === "#2 Request" - ```json - { - "level": "INFO", - "message": "Collecting payment", - "timestamp": "2021-05-03 11:47:12,494+0200", - "service": "payment", - "coldStart": true, - "functionName": "test", - "functionMemorySize": 128, - "functionArn": "arn:aws:lambda:eu-west-1:12345678910:function:test", - "function_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72" - } + ```json hl_lines="7" + { + "level": "INFO", + "message": "Updating card information", + "service": "card", + "timestamp": "2023-12-01T14:49:20.213Z", + "xray_trace_id": "2-7a518f43-5e9d2b1f6cfd5e8b3a4e1f9c", + "cardNumber": "7201 6897 6685 3285" + } ``` -## Override default object mapper +???+ 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`. -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" +## Logging incoming event - ```java hl_lines="9 10" - /** - * Handler for requests to Lambda function. - */ - public class App implements RequestHandler { +When debugging in non-production environments, you can instruct the `@Logging` annotation to log the incoming event with `logEvent` param or via `POWERTOOLS_LOGGER_LOG_EVENT` env var. + +???+ warning + This is disabled by default to prevent sensitive info being logged + +=== "AppLogEvent.java" + + ```java hl_lines="5" + public class AppLogEvent implements RequestHandler { - Logger log = LogManager.getLogger(App.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AppLogEvent.class); + + @Logging(logEvent = true) + public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { + // ... + } + } + ``` - static { - ObjectMapper objectMapper = new ObjectMapper(); - LoggingUtils.defaultObjectMapper(objectMapper); +???+ note + If you use this on a RequestStreamHandler, Powertools must duplicate input streams in order to log them. + +## Logging handler response + +When debugging in non-production environments, you can instruct the `@Logging` annotation to log the response with `logResponse` param or via `POWERTOOLS_LOGGER_LOG_RESPONSE` env var. + +???+ warning + This is disabled by default to prevent sensitive info being logged + +=== "AppLogResponse.java" + + ```java hl_lines="5" + public class AppLogResponse implements RequestHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(AppLogResponse.class); + + @Logging(logResponse = true) + public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { + // ... } + } + ``` + +???+ note + If you use this on a RequestStreamHandler, Powertools must duplicate output streams in order to log them. + +## Logging handler uncaught exception +By default, AWS Lambda logs any uncaught exception that might happen in the handler. However, this log is not structured +and does not contain any additional context. You can instruct the `@Logging` annotation to log this kind of exception +with `logError` param or via `POWERTOOLS_LOGGER_LOG_ERROR` env var. + +???+ warning + This is disabled by default to prevent double logging + +=== "AppLogResponse.java" + + ```java hl_lines="5" + public class AppLogError implements RequestHandler { - @Logging(logEvent = true) + private static final Logger LOGGER = LoggerFactory.getLogger(AppLogError.class); + + @Logging(logError = true) public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { - ... + // ... } } ``` +# Advanced + ## Sampling debug logs -You can dynamically set a percentage of your logs to **DEBUG** level via env var `POWERTOOLS_LOGGER_SAMPLE_RATE` or -via `samplingRate` attribute on annotation. +You can dynamically set a percentage of your logs to`DEBUG` level to be included in the logger output, regardless of configured log leve, using the`POWERTOOLS_LOGGER_SAMPLE_RATE` environment variable or +via `samplingRate` attribute on the `@Logging` annotation. !!! info Configuration on environment variable is given precedence over sampling rate configuration on annotation, provided it's in valid value range. === "Sampling via annotation attribute" - ```java hl_lines="8" - /** - * Handler for requests to Lambda function. - */ + ```java hl_lines="5" public class App implements RequestHandler { - Logger log = LogManager.getLogger(App.class); + private static final Logger LOGGER = LoggerFactory.getLogger(App.class); @Logging(samplingRate = 0.5) public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { - ... + // will eventually be logged based on the sampling rate + LOGGER.debug("Handle payment"); } } ``` === "Sampling via environment variable" - ```yaml hl_lines="9" + ```yaml hl_lines="8" Resources: - HelloWorldFunction: - Type: AWS::Serverless::Function - Properties: - ... - Runtime: java8 - Environment: - Variables: - POWERTOOLS_LOGGER_SAMPLE_RATE: 0.5 + PaymentFunction: + Type: AWS::Serverless::Function + Properties: + ... + Environment: + Variables: + POWERTOOLS_LOGGER_SAMPLE_RATE: 0.5 + ``` -## AWS Lambda Advanced Logging Controls (ALC) +## Built-in Correlation ID expressions -!!!question "When is it useful?" - When you want to set a logging policy to drop informational or verbose logs for one or all AWS Lambda functions, regardless of runtime and logger used. +You can use any of the following built-in JMESPath expressions as part of `@Logging(correlationIdPath = ...)`: - -With [AWS Lambda Advanced Logging Controls (ALC)](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-advanced){target="_blank"}, you can enforce a minimum log level that Lambda will accept from your application code. +???+ note "Note: Any object key named with `-` must be escaped" + For example, **`request.headers."x-amzn-trace-id"`**. -When enabled, you should keep `Logger` and ALC log level in sync to avoid data loss. +| Name | Expression | Description | +|-------------------------------|-------------------------------------|---------------------------------| +| **API_GATEWAY_REST** | `"requestContext.requestId"` | API Gateway REST API request ID | +| **API_GATEWAY_HTTP** | `"requestContext.requestId"` | API Gateway HTTP API request ID | +| **APPSYNC_RESOLVER** | `request.headers."x-amzn-trace-id"` | AppSync X-Ray Trace ID | +| **APPLICATION_LOAD_BALANCER** | `headers."x-amzn-trace-id"` | ALB X-Ray Trace ID | +| **EVENT_BRIDGE** | `"id"` | EventBridge Event ID | -Here's a sequence diagram to demonstrate how ALC will drop both `INFO` and `DEBUG` logs emitted from `Logger`, when ALC log level is stricter than `Logger`. - -```mermaid -sequenceDiagram - participant Lambda service - participant Lambda function - participant Application Logger +## Customising fields in logs - Note over Lambda service: AWS_LAMBDA_LOG_LEVEL="WARN" - Note over Application Logger: POWERTOOLS_LOG_LEVEL="DEBUG" +Powertools for AWS Lambda comes with default json structure ([standard fields](#standard-structured-keys) & [lambda context fields](#logging-lambda-context-information)). - Lambda service->>Lambda function: Invoke (event) - Lambda function->>Lambda function: Calls handler - Lambda function->>Application Logger: logger.error("Something happened") - Lambda function-->>Application Logger: logger.debug("Something happened") - Lambda function-->>Application Logger: logger.info("Something happened") - Lambda service--xLambda service: DROP INFO and DEBUG logs - Lambda service->>CloudWatch Logs: Ingest error logs +You can go further and customize which fields you want to keep in your logs or not. The configuration varies according to the underlying logging library. + +### Log4j2 configuration +Log4j2 configuration is done in _log4j2.xml_ and leverages `JsonTemplateLayout`: + +```xml + + + ``` -### Priority of log level settings in Powertools for AWS Lambda +The `JsonTemplateLayout` is automatically configured with the provided template: -We prioritise log level settings in this order: +??? example "LambdaJsonLayout.json" + ```json + { + "level": { + "$resolver": "level", + "field": "name" + }, + "message": { + "$resolver": "powertools", + "field": "message" + }, + "error": { + "message": { + "$resolver": "exception", + "field": "message" + }, + "name": { + "$resolver": "exception", + "field": "className" + }, + "stack": { + "$resolver": "exception", + "field": "stackTrace", + "stackTrace": { + "stringified": true + } + } + }, + "cold_start": { + "$resolver": "powertools", + "field": "cold_start" + }, + "function_arn": { + "$resolver": "powertools", + "field": "function_arn" + }, + "function_memory_size": { + "$resolver": "powertools", + "field": "function_memory_size" + }, + "function_name": { + "$resolver": "powertools", + "field": "function_name" + }, + "function_request_id": { + "$resolver": "powertools", + "field": "function_request_id" + }, + "function_version": { + "$resolver": "powertools", + "field": "function_version" + }, + "sampling_rate": { + "$resolver": "powertools", + "field": "sampling_rate" + }, + "service": { + "$resolver": "powertools", + "field": "service" + }, + "timestamp": { + "$resolver": "timestamp", + "pattern": { + "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + }, + "xray_trace_id": { + "$resolver": "powertools", + "field": "xray_trace_id" + }, + "": { + "$resolver": "powertools" + } + } + ``` -1. `AWS_LAMBDA_LOG_LEVEL` environment variable -2. `POWERTOOLS_LOG_LEVEL` environment variable +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: -If you set `Logger` level lower than ALC, we will emit a warning informing you that your messages will be discarded by Lambda. +#### 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. -> **NOTE** -> -> With ALC enabled, we are unable to increase the minimum log level below the `AWS_LAMBDA_LOG_LEVEL` environment variable value, see [AWS Lambda service documentation](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-log-level){target="_blank"} for more details. +=== "my-custom-template.json" -### Timestamp format + ```json + { + "message": { + "$resolver": "powertools", + "field": "message", + "asJson": true + } + } + ``` -When the Advanced Logging Controls feature is enabled, Powertools for AWS Lambda must comply with the timestamp format required by AWS Lambda, which is [RFC3339](https://www.rfc-editor.org/rfc/rfc3339). -In this case the format will be `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'`. +#### Customising date format -## Upgrade to JsonTemplateLayout from deprecated LambdaJsonLayout configuration in log4j2.xml +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 update your template.json or by configuring `log4j2.component.properties` as shown in examples below: -Prior to version [1.10.0](https://github.com/aws-powertools/powertools-lambda-java/releases/tag/v1.10.0), only supported way of configuring `log4j2.xml` was via ``. This plugin is -deprecated now and will be removed in future version. Switching to `JsonTemplateLayout` is straight forward. +=== "my-custom-template.json" -Below examples shows deprecated and new configuration of `log4j2.xml`. + ```json + { + "timestamp": { + "$resolver": "timestamp", + "pattern": { + "format": "yyyy-MM-dd HH:mm:ss", + "timeZone": "Europe/Paris", + } + }, + } + ``` -=== "Deprecated configuration of log4j2.xml" +=== "log4j2.component.properties" - ```xml hl_lines="5" - - - - - - - - - - - - - - - - + ```properties hl_lines="1 2" + log4j.layout.jsonTemplate.timestampFormatPattern=yyyy-MM-dd'T'HH:mm:ss.SSSZz + log4j.layout.jsonTemplate.timeZone=Europe/Oslo + ``` + +See [`TimestampResolver` documentation](https://logging.apache.org/log4j/2.x/manual/json-template-layout.html#event-template-resolver-timestamp){target="_blank"} for more details. + +???+ warning "Lambda Advanced Logging Controls date format" + When using the Lambda ALC, you must have a date format compatible with the [RFC3339](https://www.rfc-editor.org/rfc/rfc3339) + +#### More customization +You can also customize how [exceptions are logged](https://logging.apache.org/log4j/2.x/manual/json-template-layout.html#event-template-resolver-exception){target="_blank"}, and much more. +See the [JSON Layout template documentation](https://logging.apache.org/log4j/2.x/manual/json-template-layout.html){target="_blank"} for more details. + +### Logback configuration +Logback configuration is done in _logback.xml_ and the Powertools [`LambdaJsonEncoder`](): + +```xml + + + + +``` + +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: + +```xml + + yyyy-MM-dd HH:mm:ss + Europe/Paris + +``` + +#### More customization + +- You can use a standard `ThrowableHandlingConverter` to customize the exception format (default is no converter). Example: + +```xml + + + 30 + 2048 + 20 + sun\.reflect\..*\.invoke.* + net\.sf\.cglib\.proxy\.MethodProxy\.invoke + + true + true + + +``` + +- You can choose to add information about threads (default is `false`): + +```xml + + true + +``` + +- You can even choose to remove Powertools information from the logs like function name, arn: + +```xml + + false + +``` + +## 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) { + // ... + } + } ``` -=== "New configuration of log4j2.xml" +## 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. +The field emitted in logs will follow specs from [ECS](https://www.elastic.co/guide/en/ecs/current/ecs-reference.html){target="_blank"} together with field captured by utility as mentioned [above](#standard-structured-keys). + +### Log4j2 configuration + +Use `LambdaEcsLayout.json` as `eventTemplateUri` when configuring `JsonTemplateLayout`. + +=== "log4j2.xml" ```xml hl_lines="5" - + - - - @@ -694,3 +1125,20 @@ Below examples shows deprecated and new configuration of `log4j2.xml`. ``` +### Logback configuration + +Use the `LambdaEcsEncoder` rather than the `LambdaJsonEncoder` when configuring the appender: + +=== "logback.xml" + + ```xml hl_lines="3" + + + + + + + + + + ``` \ No newline at end of file diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index d135d7210..dc08ef51e 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -1,5 +1,5 @@ .md-grid { - max-width: 81vw + max-width: 90vw } .highlight .hll { diff --git a/examples/pom.xml b/examples/pom.xml index 3526be2a4..943cad950 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -23,7 +23,7 @@ 2.0.0-SNAPSHOT pom - Powertools for AWS Lambda (Java) library Examples + Powertools for AWS Lambda (Java) - Examples A suite of examples accompanying for Powertools for AWS Lambda (Java). diff --git a/examples/powertools-examples-batch/pom.xml b/examples/powertools-examples-batch/pom.xml index 36660bc96..a1b4c0bbc 100644 --- a/examples/powertools-examples-batch/pom.xml +++ b/examples/powertools-examples-batch/pom.xml @@ -8,13 +8,12 @@ 2.0.0-SNAPSHOT powertools-examples-batch jar - Powertools for AWS Lambda (Java) library Examples - Batch + Powertools for AWS Lambda (Java) - Examples - Batch - 2.20.0 11 11 - 1.9.20 + 1.9.20.1 2.21.1 @@ -26,7 +25,7 @@ software.amazon.lambda - powertools-logging + powertools-logging-log4j ${project.version} diff --git a/examples/powertools-examples-batch/src/main/java/org/demo/batch/dynamo/DynamoDBStreamBatchHandler.java b/examples/powertools-examples-batch/src/main/java/org/demo/batch/dynamo/DynamoDBStreamBatchHandler.java index 988c49e86..e3c27c093 100644 --- a/examples/powertools-examples-batch/src/main/java/org/demo/batch/dynamo/DynamoDBStreamBatchHandler.java +++ b/examples/powertools-examples-batch/src/main/java/org/demo/batch/dynamo/DynamoDBStreamBatchHandler.java @@ -4,14 +4,14 @@ import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.DynamodbEvent; import com.amazonaws.services.lambda.runtime.events.StreamsEventResponse; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.batch.BatchMessageHandlerBuilder; import software.amazon.lambda.powertools.batch.handler.BatchMessageHandler; public class DynamoDBStreamBatchHandler implements RequestHandler { - private final static Logger LOGGER = LogManager.getLogger(DynamoDBStreamBatchHandler.class); + private static final Logger LOGGER = LoggerFactory.getLogger(DynamoDBStreamBatchHandler.class); private final BatchMessageHandler handler; public DynamoDBStreamBatchHandler() { diff --git a/examples/powertools-examples-batch/src/main/java/org/demo/batch/dynamo/DynamoDBWriter.java b/examples/powertools-examples-batch/src/main/java/org/demo/batch/dynamo/DynamoDBWriter.java index 953ba8f23..fc1b0747b 100644 --- a/examples/powertools-examples-batch/src/main/java/org/demo/batch/dynamo/DynamoDBWriter.java +++ b/examples/powertools-examples-batch/src/main/java/org/demo/batch/dynamo/DynamoDBWriter.java @@ -8,8 +8,8 @@ import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.IntStream; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.demo.batch.model.DdbProduct; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; @@ -21,7 +21,7 @@ public class DynamoDBWriter implements RequestHandler { - private static final Logger LOGGER = LogManager.getLogger(DynamoDBWriter.class); + private static final Logger LOGGER = LoggerFactory.getLogger(DynamoDBWriter.class); private final DynamoDbEnhancedClient enhancedClient; diff --git a/examples/powertools-examples-batch/src/main/java/org/demo/batch/kinesis/KinesisBatchHandler.java b/examples/powertools-examples-batch/src/main/java/org/demo/batch/kinesis/KinesisBatchHandler.java index b188df501..f5d7102b5 100644 --- a/examples/powertools-examples-batch/src/main/java/org/demo/batch/kinesis/KinesisBatchHandler.java +++ b/examples/powertools-examples-batch/src/main/java/org/demo/batch/kinesis/KinesisBatchHandler.java @@ -4,15 +4,15 @@ import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.KinesisEvent; import com.amazonaws.services.lambda.runtime.events.StreamsEventResponse; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.demo.batch.model.Product; import software.amazon.lambda.powertools.batch.BatchMessageHandlerBuilder; import software.amazon.lambda.powertools.batch.handler.BatchMessageHandler; public class KinesisBatchHandler implements RequestHandler { - private final static Logger LOGGER = LogManager.getLogger(KinesisBatchHandler.class); + private static final Logger LOGGER = LoggerFactory.getLogger(KinesisBatchHandler.class); private final BatchMessageHandler handler; public KinesisBatchHandler() { diff --git a/examples/powertools-examples-batch/src/main/java/org/demo/batch/kinesis/KinesisBatchSender.java b/examples/powertools-examples-batch/src/main/java/org/demo/batch/kinesis/KinesisBatchSender.java index 0bc7dc42c..dadead1a2 100644 --- a/examples/powertools-examples-batch/src/main/java/org/demo/batch/kinesis/KinesisBatchSender.java +++ b/examples/powertools-examples-batch/src/main/java/org/demo/batch/kinesis/KinesisBatchSender.java @@ -10,8 +10,8 @@ import java.security.SecureRandom; import java.util.List; import java.util.stream.IntStream; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.demo.batch.model.Product; import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; @@ -28,7 +28,7 @@ */ public class KinesisBatchSender implements RequestHandler { - private static final Logger LOGGER = LogManager.getLogger(KinesisBatchSender.class); + private static final Logger LOGGER = LoggerFactory.getLogger(KinesisBatchSender.class); private final KinesisClient kinesisClient; private final SecureRandom random; diff --git a/examples/powertools-examples-batch/src/main/java/org/demo/batch/sqs/SqsBatchHandler.java b/examples/powertools-examples-batch/src/main/java/org/demo/batch/sqs/SqsBatchHandler.java index bb9d704d3..27689485c 100644 --- a/examples/powertools-examples-batch/src/main/java/org/demo/batch/sqs/SqsBatchHandler.java +++ b/examples/powertools-examples-batch/src/main/java/org/demo/batch/sqs/SqsBatchHandler.java @@ -4,14 +4,14 @@ import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse; import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.demo.batch.model.Product; import software.amazon.lambda.powertools.batch.BatchMessageHandlerBuilder; import software.amazon.lambda.powertools.batch.handler.BatchMessageHandler; public class SqsBatchHandler implements RequestHandler { - private final static Logger LOGGER = LogManager.getLogger(SqsBatchHandler.class); + private static final Logger LOGGER = LoggerFactory.getLogger(SqsBatchHandler.class); private final BatchMessageHandler handler; public SqsBatchHandler() { diff --git a/examples/powertools-examples-batch/src/main/java/org/demo/batch/sqs/SqsBatchSender.java b/examples/powertools-examples-batch/src/main/java/org/demo/batch/sqs/SqsBatchSender.java index af78bed5a..4050ab98b 100644 --- a/examples/powertools-examples-batch/src/main/java/org/demo/batch/sqs/SqsBatchSender.java +++ b/examples/powertools-examples-batch/src/main/java/org/demo/batch/sqs/SqsBatchSender.java @@ -10,8 +10,8 @@ import java.security.SecureRandom; import java.util.List; import java.util.stream.IntStream; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.demo.batch.model.Product; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; import software.amazon.awssdk.services.sqs.SqsClient; @@ -27,7 +27,7 @@ */ public class SqsBatchSender implements RequestHandler { - private static final Logger LOGGER = LogManager.getLogger(SqsBatchSender.class); + private static final Logger LOGGER = LoggerFactory.getLogger(SqsBatchSender.class); private final SqsClient sqsClient; private final SecureRandom random; diff --git a/examples/powertools-examples-cloudformation/pom.xml b/examples/powertools-examples-cloudformation/pom.xml index 864ea5fe6..bee97e52a 100644 --- a/examples/powertools-examples-cloudformation/pom.xml +++ b/examples/powertools-examples-cloudformation/pom.xml @@ -7,16 +7,15 @@ powertools-examples-cloudformation jar - Powertools for AWS Lambda (Java) library Examples - CloudFormation + Powertools for AWS Lambda (Java) - Examples - CloudFormation - 2.20.0 11 11 1.2.3 3.11.3 2.21.0 - 1.9.20 + 1.9.20.1 @@ -49,19 +48,9 @@ software.amazon.lambda - powertools-logging + powertools-logging-log4j ${project.version} - - org.apache.logging.log4j - log4j-core - ${log4j.version} - - - org.apache.logging.log4j - log4j-api - ${log4j.version} - org.aspectj aspectjrt @@ -91,14 +80,6 @@ - - org.apache.logging.log4j - log4j-jcl - ${log4j.version} - - - - diff --git a/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java b/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java index 54f13244c..c35ad8fb9 100644 --- a/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-cloudformation/src/main/java/helloworld/App.java @@ -3,8 +3,8 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; import java.util.Objects; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.awssdk.awscore.exception.AwsServiceException; import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.core.waiters.WaiterResponse; @@ -24,7 +24,7 @@ */ public class App extends AbstractCustomResourceHandler { - private final static Logger log = LogManager.getLogger(App.class); + private static final Logger log = LoggerFactory.getLogger(App.class); private final S3Client s3Client; public App() { @@ -47,7 +47,7 @@ protected Response create(CloudFormationCustomResourceEvent cloudFormationCustom Objects.requireNonNull(cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName"), "BucketName cannot be null."); - log.info(cloudFormationCustomResourceEvent); + log.info(cloudFormationCustomResourceEvent.toString()); String bucketName = (String) cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName"); log.info("Bucket Name {}", bucketName); try { @@ -57,7 +57,7 @@ protected Response create(CloudFormationCustomResourceEvent cloudFormationCustom return Response.success(bucketName); } catch (AwsServiceException | SdkClientException e) { // In case of error, return a failed response, with the bucketName as the physicalResourceId - log.error(e); + log.error("Unable to create bucket", e); return Response.failed(bucketName); } } @@ -77,7 +77,7 @@ protected Response update(CloudFormationCustomResourceEvent cloudFormationCustom Objects.requireNonNull(cloudFormationCustomResourceEvent.getResourceProperties().get("BucketName"), "BucketName cannot be null."); - log.info(cloudFormationCustomResourceEvent); + log.info(cloudFormationCustomResourceEvent.toString()); // Get the physicalResourceId. physicalResourceId is the value returned to CloudFormation in the Create request, and passed in on subsequent requests (e.g. UPDATE or DELETE) String physicalResourceId = cloudFormationCustomResourceEvent.getPhysicalResourceId(); log.info("Physical Resource ID {}", physicalResourceId); @@ -94,7 +94,7 @@ protected Response update(CloudFormationCustomResourceEvent cloudFormationCustom // Return a successful response with the newBucketName return Response.success(newBucketName); } catch (AwsServiceException | SdkClientException e) { - log.error(e); + log.error("Unable to create bucket", e); return Response.failed(newBucketName); } } else { @@ -120,7 +120,7 @@ protected Response delete(CloudFormationCustomResourceEvent cloudFormationCustom Objects.requireNonNull(cloudFormationCustomResourceEvent.getPhysicalResourceId(), "PhysicalResourceId cannot be null."); - log.info(cloudFormationCustomResourceEvent); + log.info(cloudFormationCustomResourceEvent.toString()); // Get the physicalResourceId. physicalResourceId is the value provided to CloudFormation in the Create request. String bucketName = cloudFormationCustomResourceEvent.getPhysicalResourceId(); log.info("Bucket Name {}", bucketName); @@ -135,7 +135,7 @@ protected Response delete(CloudFormationCustomResourceEvent cloudFormationCustom return Response.success(bucketName); } catch (AwsServiceException | SdkClientException e) { // Return a failed response in case of errors during the bucket deletion - log.error(e); + log.error("Unable to delete bucket", e); return Response.failed(bucketName); } } else { @@ -166,7 +166,7 @@ private void createBucket(String bucketName) { s3Client.createBucket(createBucketRequest); WaiterResponse waiterResponse = waiter.waitUntilBucketExists(HeadBucketRequest.builder().bucket(bucketName).build()); - waiterResponse.matched().response().ifPresent(log::info); + waiterResponse.matched().response().ifPresent(res -> log.info(res.toString())); log.info("Bucket Created {}", bucketName); } } \ No newline at end of file diff --git a/examples/powertools-examples-core-utilities/cdk/app/pom.xml b/examples/powertools-examples-core-utilities/cdk/app/pom.xml index 822e87633..f8d340f3b 100644 --- a/examples/powertools-examples-core-utilities/cdk/app/pom.xml +++ b/examples/powertools-examples-core-utilities/cdk/app/pom.xml @@ -2,7 +2,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 - Powertools for AWS Lambda (Java) library Examples - Core Utilities (logging, tracing, metrics) with CDK + Powertools for AWS Lambda (Java) - Examples - Core Utilities (logging, tracing, metrics) with CDK software.amazon.lambda.examples @@ -14,7 +14,7 @@ 2.20.0 11 11 - 1.9.20 + 1.9.20.1 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 988da2a73..18eea0560 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 @@ -29,8 +29,8 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.cloudwatchlogs.emf.model.DimensionSet; import software.amazon.cloudwatchlogs.emf.model.Unit; import software.amazon.lambda.powertools.logging.Logging; @@ -44,8 +44,7 @@ * 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 = LoggerFactory.getLogger(App.class); @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) diff --git a/examples/powertools-examples-core-utilities/gradle/build.gradle b/examples/powertools-examples-core-utilities/gradle/build.gradle index 03971c406..38cf96c1c 100644 --- a/examples/powertools-examples-core-utilities/gradle/build.gradle +++ b/examples/powertools-examples-core-utilities/gradle/build.gradle @@ -28,8 +28,9 @@ dependencies { implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.2.2' implementation 'com.amazonaws:aws-lambda-java-events:3.11.0' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.2' + implementation 'org.aspectj:aspectjrt:1.9.20.1' aspect 'software.amazon.lambda:powertools-tracing:2.0.0-SNAPSHOT' - aspect 'software.amazon.lambda:powertools-logging:2.0.0-SNAPSHOT' + aspect 'software.amazon.lambda:powertools-logging-log4j:2.0.0-SNAPSHOT' aspect 'software.amazon.lambda:powertools-metrics:2.0.0-SNAPSHOT' } 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 fccc63b9a..36ef72ae7 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 @@ -29,8 +29,8 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.cloudwatchlogs.emf.model.DimensionSet; import software.amazon.cloudwatchlogs.emf.model.Unit; import software.amazon.lambda.powertools.logging.Logging; @@ -44,7 +44,7 @@ * 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 = LoggerFactory.getLogger(App.class); @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) diff --git a/examples/powertools-examples-core-utilities/kotlin/build.gradle.kts b/examples/powertools-examples-core-utilities/kotlin/build.gradle.kts index 1c16db0db..b37ef6d31 100644 --- a/examples/powertools-examples-core-utilities/kotlin/build.gradle.kts +++ b/examples/powertools-examples-core-utilities/kotlin/build.gradle.kts @@ -9,16 +9,16 @@ repositories { } dependencies { - implementation("com.amazonaws:aws-lambda-java-core:1.2.2") - implementation("com.fasterxml.jackson.core:jackson-annotations:2.13.2") - implementation("com.fasterxml.jackson.core:jackson-databind:2.13.2.2") - implementation("com.amazonaws:aws-lambda-java-events:3.11.0") - implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.2") + implementation("com.amazonaws:aws-lambda-java-core:1.2.3") + implementation("com.fasterxml.jackson.core:jackson-annotations:2.15.1") + implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3") + implementation("com.amazonaws:aws-lambda-java-events:3.11.3") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.2") + implementation("org.aspectj:aspectjrt:1.9.20.1") aspect("software.amazon.lambda:powertools-tracing:2.0.0-SNAPSHOT") - aspect("software.amazon.lambda:powertools-logging:2.0.0-SNAPSHOT") + aspect("software.amazon.lambda:powertools-logging-log4j:2.0.0-SNAPSHOT") aspect("software.amazon.lambda:powertools-metrics:2.0.0-SNAPSHOT") - testImplementation("junit:junit:4.13.2") - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10") } kotlin { 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 ed4cf267a..1c925d4f4 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,7 +18,8 @@ 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.apache.logging.log4j.LogManager +import org.slf4j.Logger +import org.slf4j.LoggerFactory import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger import software.amazon.cloudwatchlogs.emf.model.DimensionSet import software.amazon.cloudwatchlogs.emf.model.Unit @@ -92,5 +93,5 @@ class App : RequestHandler 4.0.0 - Powertools for AWS Lambda (Java) library Examples - Core Utilities (logging, tracing, metrics) with SAM + Powertools for AWS Lambda (Java) - Examples - Core Utilities (logging, tracing, metrics) with SAM software.amazon.lambda.examples 2.0.0-SNAPSHOT powertools-examples-core-utilities-sam jar - 2.20.0 11 11 - 1.9.20 + 1.9.20.1 @@ -23,7 +22,7 @@ software.amazon.lambda - powertools-logging + powertools-logging-log4j ${project.version} @@ -41,16 +40,6 @@ aws-lambda-java-events 3.11.3 - - org.apache.logging.log4j - log4j-core - ${log4j.version} - - - org.apache.logging.log4j - log4j-api - ${log4j.version} - org.aspectj aspectjrt diff --git a/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java index fccc63b9a..36ef72ae7 100644 --- a/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-core-utilities/sam/src/main/java/helloworld/App.java @@ -29,8 +29,8 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.cloudwatchlogs.emf.model.DimensionSet; import software.amazon.cloudwatchlogs.emf.model.Unit; import software.amazon.lambda.powertools.logging.Logging; @@ -44,7 +44,7 @@ * 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 = LoggerFactory.getLogger(App.class); @Logging(logEvent = true, samplingRate = 0.7) @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) diff --git a/examples/powertools-examples-core-utilities/serverless/pom.xml b/examples/powertools-examples-core-utilities/serverless/pom.xml index 7ebfdfc00..32cca9bb4 100644 --- a/examples/powertools-examples-core-utilities/serverless/pom.xml +++ b/examples/powertools-examples-core-utilities/serverless/pom.xml @@ -2,17 +2,16 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 - Powertools for AWS Lambda (Java) library Examples - Core Utilities (logging, tracing, metrics) with Serverless + Powertools for AWS Lambda (Java) - Examples - Core Utilities (logging, tracing, metrics) with Serverless software.amazon.lambda.examples 2.0.0-SNAPSHOT powertools-examples-core-utilities-serverless jar - 2.20.0 11 11 - 1.9.20 + 1.9.20.1 @@ -23,7 +22,7 @@ software.amazon.lambda - powertools-logging + powertools-logging-log4j ${project.version} @@ -41,16 +40,6 @@ aws-lambda-java-events 3.11.3 - - org.apache.logging.log4j - log4j-core - ${log4j.version} - - - org.apache.logging.log4j - log4j-api - ${log4j.version} - org.aspectj aspectjrt diff --git a/examples/powertools-examples-core-utilities/terraform/pom.xml b/examples/powertools-examples-core-utilities/terraform/pom.xml index f508d9d3d..c6f838619 100644 --- a/examples/powertools-examples-core-utilities/terraform/pom.xml +++ b/examples/powertools-examples-core-utilities/terraform/pom.xml @@ -2,17 +2,16 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 - Powertools for AWS Lambda (Java) library Examples - Core Utilities (logging, tracing, metrics) with Terraform + Powertools for AWS Lambda (Java) - Examples - Core Utilities (logging, tracing, metrics) with Terraform software.amazon.lambda.examples 2.0.0-SNAPSHOT powertools-examples-core-utilities-terraform jar - 2.20.0 11 11 - 1.9.20 + 1.9.20.1 @@ -23,7 +22,7 @@ software.amazon.lambda - powertools-logging + powertools-logging-log4j ${project.version} @@ -41,16 +40,6 @@ aws-lambda-java-events 3.11.3 - - org.apache.logging.log4j - log4j-core - ${log4j.version} - - - org.apache.logging.log4j - log4j-api - ${log4j.version} - org.aspectj aspectjrt diff --git a/examples/powertools-examples-idempotency/pom.xml b/examples/powertools-examples-idempotency/pom.xml index b7fd3d832..5a040fec0 100644 --- a/examples/powertools-examples-idempotency/pom.xml +++ b/examples/powertools-examples-idempotency/pom.xml @@ -20,13 +20,12 @@ 2.0.0-SNAPSHOT powertools-examples-idempotency jar - Powertools for AWS Lambda (Java) library Examples - Idempotency + Powertools for AWS Lambda (Java) - Examples - Idempotency - 2.20.0 11 11 - 1.9.20 + 1.9.20.1 @@ -37,7 +36,7 @@ software.amazon.lambda - powertools-logging + powertools-logging-log4j ${project.version} @@ -55,16 +54,6 @@ aws-lambda-java-events 3.11.3 - - org.apache.logging.log4j - log4j-core - ${log4j.version} - - - org.apache.logging.log4j - log4j-api - ${log4j.version} - org.aspectj aspectjrt @@ -83,11 +72,6 @@ 5.9.3 test - - com.amazonaws - aws-lambda-java-tests - 1.1.1 - diff --git a/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java b/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java index 72fa621ad..cf0c0ee31 100644 --- a/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java @@ -26,8 +26,8 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.lambda.powertools.idempotency.Idempotency; import software.amazon.lambda.powertools.idempotency.IdempotencyConfig; @@ -37,7 +37,7 @@ import software.amazon.lambda.powertools.utilities.JsonConfig; public class App implements RequestHandler { - private final static Logger log = LogManager.getLogger(App.class); + private static final Logger log = LoggerFactory.getLogger(App.class); public App() { this(null); diff --git a/examples/powertools-examples-parameters/pom.xml b/examples/powertools-examples-parameters/pom.xml index 8d7fbd4f4..3630811f3 100644 --- a/examples/powertools-examples-parameters/pom.xml +++ b/examples/powertools-examples-parameters/pom.xml @@ -5,18 +5,18 @@ 2.0.0-SNAPSHOT powertools-examples-parameters jar - Powertools for AWS Lambda (Java) library Examples - Parameters + Powertools for AWS Lambda (Java) - Examples - Parameters 11 11 - 1.9.20 + 1.9.20.1 software.amazon.lambda - powertools-logging + powertools-logging-log4j ${project.version} diff --git a/examples/powertools-examples-parameters/src/main/java/org/demo/parameters/ParametersFunction.java b/examples/powertools-examples-parameters/src/main/java/org/demo/parameters/ParametersFunction.java index 9a1d3636b..9c3c422cf 100644 --- a/examples/powertools-examples-parameters/src/main/java/org/demo/parameters/ParametersFunction.java +++ b/examples/powertools-examples-parameters/src/main/java/org/demo/parameters/ParametersFunction.java @@ -29,15 +29,15 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.parameters.secrets.SecretsParam; import software.amazon.lambda.powertools.parameters.secrets.SecretsProvider; import software.amazon.lambda.powertools.parameters.ssm.SSMParam; import software.amazon.lambda.powertools.parameters.ssm.SSMProvider; public class ParametersFunction implements RequestHandler { - private final static Logger log = LogManager.getLogger(ParametersFunction.class); + private static final Logger log = LoggerFactory.getLogger(ParametersFunction.class); // Annotation-style injection from secrets manager @SecretsParam(key = "/powertools-java/userpwd") diff --git a/examples/powertools-examples-serialization/pom.xml b/examples/powertools-examples-serialization/pom.xml index 468c7d161..2c8fc951a 100644 --- a/examples/powertools-examples-serialization/pom.xml +++ b/examples/powertools-examples-serialization/pom.xml @@ -5,7 +5,7 @@ 2.0.0-SNAPSHOT powertools-examples-serialization jar - Powertools for AWS Lambda (Java) library Examples - Serialization + Powertools for AWS Lambda (Java) - Examples - Serialization 11 @@ -15,7 +15,7 @@ software.amazon.lambda - powertools-logging + powertools-logging-log4j ${project.version} diff --git a/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/APIGatewayRequestDeserializationFunction.java b/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/APIGatewayRequestDeserializationFunction.java index e70b37959..3ca75cf4a 100644 --- a/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/APIGatewayRequestDeserializationFunction.java +++ b/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/APIGatewayRequestDeserializationFunction.java @@ -22,18 +22,21 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import java.util.HashMap; import java.util.Map; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class APIGatewayRequestDeserializationFunction implements RequestHandler { - private final static Logger LOGGER = LogManager.getLogger(APIGatewayRequestDeserializationFunction.class); - private static final Map HEADERS = new HashMap() {{ + private static final Logger LOGGER = LoggerFactory.getLogger(APIGatewayRequestDeserializationFunction.class); + private static final Map HEADERS = new HashMap() { + private static final long serialVersionUID = 7074189990115081999L; + { put("Content-Type", "application/json"); put("X-Custom-Header", "application/json"); - }}; + } + }; public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent event, Context context) { diff --git a/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/SQSEventDeserializationFunction.java b/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/SQSEventDeserializationFunction.java index 36dbed074..79097e19c 100644 --- a/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/SQSEventDeserializationFunction.java +++ b/examples/powertools-examples-serialization/src/main/java/org/demo/serialization/SQSEventDeserializationFunction.java @@ -20,13 +20,13 @@ import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.SQSEvent; import java.util.List; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class SQSEventDeserializationFunction implements RequestHandler { - private final static Logger LOGGER = LogManager.getLogger(SQSEventDeserializationFunction.class); + private static final Logger LOGGER = LoggerFactory.getLogger(SQSEventDeserializationFunction.class); public String handleRequest(SQSEvent event, Context context) { List products = extractDataFrom(event).asListOf(Product.class); diff --git a/examples/powertools-examples-validation/pom.xml b/examples/powertools-examples-validation/pom.xml index 2d4ef07a0..ed33568cb 100644 --- a/examples/powertools-examples-validation/pom.xml +++ b/examples/powertools-examples-validation/pom.xml @@ -19,18 +19,18 @@ 2.0.0-SNAPSHOT powertools-examples-validation jar - Powertools for AWS Lambda (Java) library Examples - Validation + Powertools for AWS Lambda (Java) - Examples - Validation 1.8 1.8 - 1.9.20 + 1.9.20.1 software.amazon.lambda - powertools-logging + powertools-logging-log4j ${project.version} diff --git a/pom.xml b/pom.xml index 1d35a990f..b7227cf8a 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ 2.0.0-SNAPSHOT pom - Powertools for AWS Lambda (Java) library Parent + Powertools for AWS Lambda (Java) - Parent A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, structured logging and creating custom metrics asynchronously easier. @@ -44,6 +44,8 @@ powertools-common powertools-serialization powertools-logging + powertools-logging/powertools-logging-log4j + powertools-logging/powertools-logging-logback powertools-tracing powertools-metrics powertools-parameters @@ -76,7 +78,8 @@ 1.8 1.8 - 2.20.0 + 2.22.0 + 2.0.7 2.15.3 2.21.0 2.14.0 @@ -96,7 +99,8 @@ 3.1.0 5.10.0 1.0.6 - 0.5.1 + 0.6.0 + 1.5.0 @@ -123,6 +127,16 @@ powertools-logging ${project.version} + + software.amazon.lambda + powertools-logging-log4j + ${project.version} + + + software.amazon.lambda + powertools-logging-logback + ${project.version} + software.amazon.lambda powertools-sqs @@ -200,6 +214,11 @@ log4j-core ${log4j.version} + + org.apache.logging.log4j + log4j-slf4j-impl + ${log4j.version} + org.apache.logging.log4j log4j-slf4j2-impl @@ -220,6 +239,16 @@ log4j-jcl ${log4j.version} + + co.elastic.logging + logback-ecs-encoder + ${elastic.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + com.amazonaws aws-xray-recorder-sdk-core @@ -519,7 +548,6 @@ - jdk16 @@ -605,6 +633,34 @@ + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.3.0 + + checkstyle.xml + true + true + false + + + + + + com.puppycrawl.tools + checkstyle + 10.12.3 + + + + + + check + + + + diff --git a/powertools-batch/pom.xml b/powertools-batch/pom.xml index d7246b816..eaafdb56e 100644 --- a/powertools-batch/pom.xml +++ b/powertools-batch/pom.xml @@ -10,7 +10,7 @@ A suite of utilities that makes batch message processing using AWS Lambda easier. - Powertools for AWS Lambda (Java) batch messages + Powertools for AWS Lambda (Java) - Batch messages diff --git a/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/DynamoDbBatchMessageHandler.java b/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/DynamoDbBatchMessageHandler.java index aa6eba839..83a8bf7dd 100644 --- a/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/DynamoDbBatchMessageHandler.java +++ b/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/DynamoDbBatchMessageHandler.java @@ -30,7 +30,7 @@ * @see DynamoDB Streams batch failure reporting */ public class DynamoDbBatchMessageHandler implements BatchMessageHandler { - private final static Logger LOGGER = LoggerFactory.getLogger(DynamoDbBatchMessageHandler.class); + private static final Logger LOGGER = LoggerFactory.getLogger(DynamoDbBatchMessageHandler.class); private final Consumer successHandler; private final BiConsumer failureHandler; diff --git a/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/KinesisStreamsBatchMessageHandler.java b/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/KinesisStreamsBatchMessageHandler.java index fe1aaf354..ad1dd302d 100644 --- a/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/KinesisStreamsBatchMessageHandler.java +++ b/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/KinesisStreamsBatchMessageHandler.java @@ -34,7 +34,7 @@ * @param The user-defined type of the Kinesis record payload */ public class KinesisStreamsBatchMessageHandler implements BatchMessageHandler { - private final static Logger LOGGER = LoggerFactory.getLogger(KinesisStreamsBatchMessageHandler.class); + private static final Logger LOGGER = LoggerFactory.getLogger(KinesisStreamsBatchMessageHandler.class); private final BiConsumer rawMessageHandler; private final BiConsumer messageHandler; diff --git a/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/SqsBatchMessageHandler.java b/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/SqsBatchMessageHandler.java index b3c416a69..b634f9b62 100644 --- a/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/SqsBatchMessageHandler.java +++ b/powertools-batch/src/main/java/software/amazon/lambda/powertools/batch/handler/SqsBatchMessageHandler.java @@ -31,11 +31,11 @@ * @see SQS Batch failure reporting */ public class SqsBatchMessageHandler implements BatchMessageHandler { - private final static Logger LOGGER = LoggerFactory.getLogger(SqsBatchMessageHandler.class); + private static final Logger LOGGER = LoggerFactory.getLogger(SqsBatchMessageHandler.class); // The attribute on an SQS-FIFO message used to record the message group ID // https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#sample-fifo-queues-message-event - private final static String MESSAGE_GROUP_ID_KEY = "MessageGroupId"; + private static final String MESSAGE_GROUP_ID_KEY = "MessageGroupId"; private final Class messageClass; private final BiConsumer messageHandler; diff --git a/powertools-cloudformation/pom.xml b/powertools-cloudformation/pom.xml index 0e6763e25..79071d532 100644 --- a/powertools-cloudformation/pom.xml +++ b/powertools-cloudformation/pom.xml @@ -27,7 +27,7 @@ 2.0.0-SNAPSHOT - Powertools for AWS Lambda (Java) library Cloudformation + Powertools for AWS Lambda (Java) - Cloudformation A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, structured logging and creating custom metrics asynchronously easier. diff --git a/powertools-common/pom.xml b/powertools-common/pom.xml index 82e2c1d17..363d2e944 100644 --- a/powertools-common/pom.xml +++ b/powertools-common/pom.xml @@ -27,7 +27,7 @@ 2.0.0-SNAPSHOT - Powertools for AWS Lambda (Java) library Common Internal Utilities + Powertools for AWS Lambda (Java) - Common Internal Utilities Internal utilities shared by the Powertools for AWS Lambda (Java) modules. Do not use directly in your project. https://aws.amazon.com/lambda/ @@ -55,17 +55,17 @@ - com.amazonaws - aws-lambda-java-core + org.slf4j + slf4j-api org.aspectj aspectjrt + provided - org.apache.logging.log4j - log4j-slf4j2-impl - ${log4j.version} + com.amazonaws + aws-lambda-java-core diff --git a/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/LambdaConstants.java b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/LambdaConstants.java index bdcbdc010..d27ac1aa2 100644 --- a/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/LambdaConstants.java +++ b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/LambdaConstants.java @@ -17,12 +17,6 @@ public class LambdaConstants { public static final String LAMBDA_FUNCTION_NAME_ENV = "AWS_LAMBDA_FUNCTION_NAME"; public static final String AWS_REGION_ENV = "AWS_REGION"; - // Also you can use AWS_LAMBDA_INITIALIZATION_TYPE to distinguish between on-demand and SnapStart initialization - // it's not recommended to use this env variable to initialize SDK clients or other resources. - @Deprecated - public static final String AWS_LAMBDA_INITIALIZATION_TYPE = "AWS_LAMBDA_INITIALIZATION_TYPE"; - @Deprecated - public static final String ON_DEMAND = "on-demand"; public static final String X_AMZN_TRACE_ID = "_X_AMZN_TRACE_ID"; public static final String XRAY_TRACE_HEADER = "com.amazonaws.xray.traceHeader"; public static final String AWS_SAM_LOCAL = "AWS_SAM_LOCAL"; diff --git a/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/LambdaHandlerProcessor.java b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/LambdaHandlerProcessor.java index a2830e467..bfacd5204 100644 --- a/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/LambdaHandlerProcessor.java +++ b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/LambdaHandlerProcessor.java @@ -69,7 +69,6 @@ public static boolean placedOnStreamHandler(final ProceedingJoinPoint pjp) { } public static Context extractContext(final ProceedingJoinPoint pjp) { - if (placedOnRequestHandler(pjp)) { return (Context) pjp.getArgs()[1]; } else if (placedOnStreamHandler(pjp)) { diff --git a/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/UserAgentConfigurator.java b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/UserAgentConfigurator.java index 585c38c59..8ae10ad62 100644 --- a/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/UserAgentConfigurator.java +++ b/powertools-common/src/main/java/software/amazon/lambda/powertools/common/internal/UserAgentConfigurator.java @@ -37,8 +37,8 @@ public class UserAgentConfigurator { public static final String AWS_EXECUTION_ENV = "AWS_EXECUTION_ENV"; private static final Logger LOG = LoggerFactory.getLogger(UserAgentConfigurator.class); private static final String NO_OP = "no-op"; - private static String ptVersion = getProjectVersion(); - private static String userAgentPattern = "PT/" + PT_FEATURE_VARIABLE + "/" + ptVersion + " PTEnv/" + private static final String POWERTOOLS_VERSION = getProjectVersion(); + private static final String USER_AGENT_PATTERN = "PT/" + PT_FEATURE_VARIABLE + "/" + POWERTOOLS_VERSION + " PTEnv/" + PT_EXEC_ENV_VARIABLE; private UserAgentConfigurator() { @@ -99,7 +99,7 @@ public static String getUserAgent(String ptFeature) { String awsExecutionEnv = getenv(AWS_EXECUTION_ENV); String ptExecEnv = awsExecutionEnv != null ? awsExecutionEnv : NA; - String userAgent = userAgentPattern.replace(PT_EXEC_ENV_VARIABLE, ptExecEnv); + String userAgent = USER_AGENT_PATTERN.replace(PT_EXEC_ENV_VARIABLE, ptExecEnv); if (ptFeature == null || ptFeature.isEmpty()) { ptFeature = NO_OP; diff --git a/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/LambdaHandlerProcessorTest.java b/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/LambdaHandlerProcessorTest.java index 589aab703..4ad170600 100644 --- a/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/LambdaHandlerProcessorTest.java +++ b/powertools-common/src/test/java/software/amazon/lambda/powertools/common/internal/LambdaHandlerProcessorTest.java @@ -31,9 +31,6 @@ import org.aspectj.lang.Signature; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; -import software.amazon.lambda.powertools.common.internal.LambdaConstants; -import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; -import software.amazon.lambda.powertools.common.internal.SystemWrapper; class LambdaHandlerProcessorTest { diff --git a/powertools-e2e-tests/handlers/batch/pom.xml b/powertools-e2e-tests/handlers/batch/pom.xml index 8740dcb0b..a36d464ea 100644 --- a/powertools-e2e-tests/handlers/batch/pom.xml +++ b/powertools-e2e-tests/handlers/batch/pom.xml @@ -19,7 +19,7 @@ software.amazon.lambda - powertools-logging + powertools-logging-log4j com.amazonaws @@ -29,10 +29,6 @@ com.amazonaws aws-lambda-java-serialization - - org.apache.logging.log4j - log4j-slf4j2-impl - software.amazon.awssdk dynamodb diff --git a/powertools-e2e-tests/handlers/batch/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/batch/src/main/java/software/amazon/lambda/powertools/e2e/Function.java index 64f5a02c2..bfde65bc8 100644 --- a/powertools-e2e-tests/handlers/batch/src/main/java/software/amazon/lambda/powertools/e2e/Function.java +++ b/powertools-e2e-tests/handlers/batch/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -14,7 +14,6 @@ package software.amazon.lambda.powertools.e2e; -import com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ObjectMapper; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.DynamodbEvent; @@ -24,41 +23,26 @@ import com.amazonaws.services.lambda.runtime.events.StreamsEventResponse; import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; import com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers; -import com.amazonaws.services.lambda.runtime.serialization.factories.JacksonFactory; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.JsonNode; - import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.core.util.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; import software.amazon.lambda.powertools.batch.BatchMessageHandlerBuilder; import software.amazon.lambda.powertools.batch.handler.BatchMessageHandler; import software.amazon.lambda.powertools.e2e.model.Product; -import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.utilities.JsonConfig; - -import javax.management.Attribute; public class Function implements RequestHandler { - private final static Logger LOGGER = LogManager.getLogger(Function.class); + private static final Logger LOGGER = LoggerFactory.getLogger(Function.class); private final BatchMessageHandler sqsHandler; private final BatchMessageHandler kinesisHandler; @@ -139,7 +123,7 @@ public Object createResult(String input, Context context) { SQSEvent event = serializer.fromJson(input); if (event.getRecords().get(0).getEventSource().equals("aws:sqs")) { LOGGER.info("Running for SQS"); - LOGGER.info(event); + LOGGER.info(event.toString()); return sqsHandler.processBatch(event, context); } diff --git a/powertools-e2e-tests/handlers/idempotency/pom.xml b/powertools-e2e-tests/handlers/idempotency/pom.xml index a0e6bd4fc..da2bbfb80 100644 --- a/powertools-e2e-tests/handlers/idempotency/pom.xml +++ b/powertools-e2e-tests/handlers/idempotency/pom.xml @@ -19,11 +19,7 @@ software.amazon.lambda - powertools-logging - - - org.apache.logging.log4j - log4j-slf4j2-impl + powertools-logging-log4j com.amazonaws diff --git a/powertools-e2e-tests/handlers/largemessage/pom.xml b/powertools-e2e-tests/handlers/largemessage/pom.xml index 277e76fc1..0728404bf 100644 --- a/powertools-e2e-tests/handlers/largemessage/pom.xml +++ b/powertools-e2e-tests/handlers/largemessage/pom.xml @@ -23,16 +23,12 @@ software.amazon.lambda - powertools-logging + powertools-logging-log4j com.amazonaws aws-lambda-java-events - - org.apache.logging.log4j - log4j-slf4j2-impl - org.aspectj aspectjrt diff --git a/powertools-e2e-tests/handlers/largemessage_idempotent/pom.xml b/powertools-e2e-tests/handlers/largemessage_idempotent/pom.xml index d887341c5..8cb2cb52c 100644 --- a/powertools-e2e-tests/handlers/largemessage_idempotent/pom.xml +++ b/powertools-e2e-tests/handlers/largemessage_idempotent/pom.xml @@ -23,16 +23,12 @@ software.amazon.lambda - powertools-logging + powertools-logging-log4j com.amazonaws aws-lambda-java-events - - org.apache.logging.log4j - log4j-slf4j2-impl - org.aspectj aspectjrt diff --git a/powertools-e2e-tests/handlers/logging/pom.xml b/powertools-e2e-tests/handlers/logging/pom.xml index 222c5ab2e..88feda09b 100644 --- a/powertools-e2e-tests/handlers/logging/pom.xml +++ b/powertools-e2e-tests/handlers/logging/pom.xml @@ -15,7 +15,12 @@ software.amazon.lambda - powertools-logging + powertools-logging-log4j + + + org.apache.logging.log4j + log4j-layout-template-json + 2.20.0 org.aspectj @@ -25,10 +30,6 @@ com.amazonaws aws-lambda-java-events - - org.aspectj - aspectjrt - 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 62ebabc6e..c2634533d 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 @@ -16,18 +16,16 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.logging.Logging; import software.amazon.lambda.powertools.logging.LoggingUtils; public class Function implements RequestHandler { - - private static final Logger LOG = LogManager.getLogger(Function.class); + private static final Logger LOG = LoggerFactory.getLogger(Function.class); @Logging public String handleRequest(Input input, Context context) { - LoggingUtils.appendKeys(input.getKeys()); LOG.info(input.getMessage()); diff --git a/powertools-e2e-tests/handlers/parameters/pom.xml b/powertools-e2e-tests/handlers/parameters/pom.xml index 328d30485..2d6a9a06a 100644 --- a/powertools-e2e-tests/handlers/parameters/pom.xml +++ b/powertools-e2e-tests/handlers/parameters/pom.xml @@ -15,7 +15,7 @@ software.amazon.lambda - powertools-logging + powertools-logging-log4j software.amazon.lambda diff --git a/powertools-e2e-tests/handlers/pom.xml b/powertools-e2e-tests/handlers/pom.xml index 2c73de977..412593da9 100644 --- a/powertools-e2e-tests/handlers/pom.xml +++ b/powertools-e2e-tests/handlers/pom.xml @@ -22,8 +22,7 @@ 1.13.1 3.11.0 2.20.108 - 2.20.0 - 1.9.20 + 1.9.20.1 @@ -33,7 +32,10 @@ logging tracing metrics + batch idempotency + largemessage + largemessage_idempotent parameters validation-alb-event validation-apigw-event @@ -55,7 +57,7 @@ software.amazon.lambda - powertools-logging + powertools-logging-log4j ${lambda.powertools.version} @@ -108,11 +110,6 @@ aws-lambda-java-serialization ${lambda.java.serialization} - - org.apache.logging.log4j - log4j-slf4j2-impl - ${log4j.version} - diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index 1f5bd6347..a64e7a354 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -24,7 +24,7 @@ powertools-e2e-tests - Powertools for AWS Lambda (Java)library End-to-end tests + Powertools for AWS Lambda (Java) - End-to-end tests Powertools for AWS Lambda (Java)End-To-End Tests @@ -184,6 +184,7 @@ org.apache.maven.plugins maven-deploy-plugin + 3.1.1 true diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java index b060879d3..f78500c65 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LoggingE2ET.java @@ -83,7 +83,7 @@ public void test_logInfoWithAdditionalKeys() throws JsonProcessingException { JsonNode jsonNode = objectMapper.readTree(functionLogs[0]); assertThat(jsonNode.get("message").asText()).isEqualTo("New Order"); assertThat(jsonNode.get("orderId").asText()).isEqualTo(orderId); - assertThat(jsonNode.get("coldStart").asBoolean()).isTrue(); + assertThat(jsonNode.get("cold_start").asBoolean()).isTrue(); assertThat(jsonNode.get("xray_trace_id").asText()).isNotBlank(); assertThat(jsonNode.get("function_request_id").asText()).isEqualTo(invocationResult1.getRequestId()); @@ -91,6 +91,6 @@ public void test_logInfoWithAdditionalKeys() throws JsonProcessingException { functionLogs = invocationResult2.getLogs().getFunctionLogs(INFO); assertThat(functionLogs).hasSize(1); jsonNode = objectMapper.readTree(functionLogs[0]); - assertThat(jsonNode.get("coldStart").asBoolean()).isFalse(); + assertThat(jsonNode.get("cold_start").asBoolean()).isFalse(); } } diff --git a/powertools-idempotency/pom.xml b/powertools-idempotency/pom.xml index f7f44dc99..eda7bd982 100644 --- a/powertools-idempotency/pom.xml +++ b/powertools-idempotency/pom.xml @@ -27,7 +27,7 @@ powertools-idempotency jar - Powertools for AWS Lambda (Java) library Idempotency + Powertools for AWS Lambda (Java) - Idempotency @@ -55,6 +55,11 @@ + + org.aspectj + aspectjrt + provided + software.amazon.lambda powertools-common @@ -86,10 +91,6 @@ url-connection-client ${aws.sdk.version} - - org.aspectj - aspectjrt - diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotency.java b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotency.java index 6da826c45..bd564caf8 100644 --- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotency.java +++ b/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotency.java @@ -82,7 +82,7 @@ private void setPersistenceStore(BasePersistenceStore persistenceStore) { } private static class Holder { - private final static Idempotency instance = new Idempotency(); + private static final Idempotency instance = new Idempotency(); } public static class Config { diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyFunction.java b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyFunction.java index 76c36ae9f..43e191fc2 100644 --- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyFunction.java +++ b/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyFunction.java @@ -25,8 +25,8 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.lambda.powertools.idempotency.Idempotency; import software.amazon.lambda.powertools.idempotency.IdempotencyConfig; @@ -35,7 +35,7 @@ import software.amazon.lambda.powertools.utilities.JsonConfig; public class IdempotencyFunction implements RequestHandler { - private final static Logger LOG = LogManager.getLogger(IdempotencyFunction.class); + private static final Logger LOG = LoggerFactory.getLogger(IdempotencyFunction.class); public boolean handlerExecuted = false; diff --git a/powertools-large-messages/pom.xml b/powertools-large-messages/pom.xml index a56623518..af031ff21 100644 --- a/powertools-large-messages/pom.xml +++ b/powertools-large-messages/pom.xml @@ -29,7 +29,7 @@ powertools-large-messages jar - Powertools for AWS Lambda (Java) library Large messages + Powertools for AWS Lambda (Java) - Large messages GitHub Issues @@ -54,6 +54,11 @@ + + org.aspectj + aspectjrt + provided + software.amazon.lambda powertools-common diff --git a/powertools-logging/pom.xml b/powertools-logging/pom.xml index 21a221967..56ca0e62b 100644 --- a/powertools-logging/pom.xml +++ b/powertools-logging/pom.xml @@ -17,48 +17,26 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - - powertools-logging - jar - powertools-parent software.amazon.lambda 2.0.0-SNAPSHOT - Powertools for AWS Lambda (Java) library Logging - - A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, structured logging and creating custom metrics asynchronously easier. - - https://aws.amazon.com/lambda/ - - GitHub Issues - https://github.com/aws-powertools/powertools-lambda-java/issues - - - https://github.com/aws-powertools/powertools-lambda-java.git - - - - Powertools for AWS Lambda team - Amazon Web Services - https://aws.amazon.com/ - - - - - - ossrh - https://aws.oss.sonatype.org/content/repositories/snapshots - - + powertools-logging + jar + Powertools for AWS Lambda (Java) - Logging + Set of utility for better logging - common software.amazon.lambda powertools-common + + software.amazon.lambda + powertools-serialization + com.amazonaws aws-lambda-java-core @@ -68,20 +46,13 @@ jackson-databind - org.apache.logging.log4j - log4j-layout-template-json - - - org.apache.logging.log4j - log4j-core + org.slf4j + slf4j-api - org.apache.logging.log4j - log4j-slf4j2-impl - - - org.apache.logging.log4j - log4j-api + org.aspectj + aspectjrt + provided diff --git a/powertools-logging/powertools-logging-log4j/pom.xml b/powertools-logging/powertools-logging-log4j/pom.xml new file mode 100644 index 000000000..df6154560 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/pom.xml @@ -0,0 +1,137 @@ + + + 4.0.0 + + + powertools-parent + software.amazon.lambda + 2.0.0-SNAPSHOT + ../../pom.xml + + + powertools-logging-log4j + jar + Powertools for AWS Lambda (Java) - Logging with Log4j2 + Set of utility for better logging with log4j + + + + software.amazon.lambda + powertools-logging + ${project.version} + + + org.aspectj + aspectjrt + provided + + + org.apache.logging.log4j + log4j-slf4j2-impl + + + org.apache.logging.log4j + log4j-core + + + org.apache.logging.log4j + log4j-layout-template-json + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.apache.commons + commons-lang3 + test + + + org.aspectj + aspectjweaver + test + + + org.assertj + assertj-core + test + + + com.amazonaws + aws-lambda-java-events + test + + + com.amazonaws + aws-lambda-java-tests + test + + + org.skyscreamer + jsonassert + test + + + + + + + dev.aspectj + aspectj-maven-plugin + 1.13.1 + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-logging + + + + + + + compile + + + + + + org.aspectj + aspectjtools + ${aspectj.version} + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + + testLog4j + eu-central-1 + <_X_AMZN_TRACE_ID>Root=1-63441c4a-abcdef012345678912345678 + + + + + + + \ No newline at end of file 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 new file mode 100644 index 000000000..95086a085 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolver.java @@ -0,0 +1,286 @@ +/* + * 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 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 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; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_NAME; +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.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.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.internal.PowertoolsLoggedFields; + +/** + * Custom {@link org.apache.logging.log4j.layout.template.json.resolver.TemplateResolver} + * used by {@link org.apache.logging.log4j.layout.template.json.JsonTemplateLayout} + * to be able to recognize powertools fields in the LambdaJsonLayout.json file. + */ +final class PowertoolsResolver implements EventResolver { + + private static final EventResolver COLD_START_RESOLVER = new EventResolver() { + @Override + public boolean isResolvable(LogEvent logEvent) { + final String coldStart = + logEvent.getContextData().getValue(PowertoolsLoggedFields.FUNCTION_COLD_START.getName()); + return null != coldStart; + } + + @Override + public void resolve(LogEvent logEvent, JsonWriter jsonWriter) { + final String coldStart = + logEvent.getContextData().getValue(PowertoolsLoggedFields.FUNCTION_COLD_START.getName()); + jsonWriter.writeBoolean(Boolean.parseBoolean(coldStart)); + } + }; + + private static final EventResolver FUNCTION_NAME_RESOLVER = + (final LogEvent logEvent, final JsonWriter jsonWriter) -> { + final String functionName = + logEvent.getContextData().getValue(FUNCTION_NAME.getName()); + jsonWriter.writeString(functionName); + }; + + private static final EventResolver FUNCTION_VERSION_RESOLVER = + (final LogEvent logEvent, final JsonWriter jsonWriter) -> { + final String functionVersion = + logEvent.getContextData().getValue(PowertoolsLoggedFields.FUNCTION_VERSION.getName()); + jsonWriter.writeString(functionVersion); + }; + + private static final EventResolver FUNCTION_ARN_RESOLVER = + (final LogEvent logEvent, final JsonWriter jsonWriter) -> { + final String functionArn = + logEvent.getContextData().getValue(PowertoolsLoggedFields.FUNCTION_ARN.getName()); + jsonWriter.writeString(functionArn); + }; + + private static final EventResolver FUNCTION_REQ_RESOLVER = + (final LogEvent logEvent, final JsonWriter jsonWriter) -> { + final String functionRequestId = + logEvent.getContextData().getValue(PowertoolsLoggedFields.FUNCTION_REQUEST_ID.getName()); + jsonWriter.writeString(functionRequestId); + }; + + private static final EventResolver FUNCTION_MEMORY_RESOLVER = new EventResolver() { + @Override + public boolean isResolvable(LogEvent logEvent) { + final String functionMemory = + logEvent.getContextData().getValue(PowertoolsLoggedFields.FUNCTION_MEMORY_SIZE.getName()); + return null != functionMemory; + } + + @Override + public void resolve(LogEvent logEvent, JsonWriter jsonWriter) { + final String functionMemory = + logEvent.getContextData().getValue(PowertoolsLoggedFields.FUNCTION_MEMORY_SIZE.getName()); + jsonWriter.writeNumber(Integer.parseInt(functionMemory)); + } + }; + + private static final EventResolver SAMPLING_RATE_RESOLVER = new EventResolver() { + @Override + public boolean isResolvable(LogEvent logEvent) { + final String samplingRate = + logEvent.getContextData().getValue(PowertoolsLoggedFields.SAMPLING_RATE.getName()); + try { + return (null != samplingRate && Float.parseFloat(samplingRate) > 0.f); + } catch (NumberFormatException nfe) { + return false; + } + } + + @Override + public void resolve(LogEvent logEvent, JsonWriter jsonWriter) { + final String samplingRate = + logEvent.getContextData().getValue(PowertoolsLoggedFields.SAMPLING_RATE.getName()); + jsonWriter.writeNumber(Float.parseFloat(samplingRate)); + } + }; + + private static final EventResolver XRAY_TRACE_RESOLVER = new EventResolver() { + @Override + public boolean isResolvable(LogEvent logEvent) { + final String traceId = + logEvent.getContextData().getValue(PowertoolsLoggedFields.FUNCTION_TRACE_ID.getName()); + return null != traceId; + } + + @Override + public void resolve(LogEvent logEvent, JsonWriter jsonWriter) { + final String traceId = + logEvent.getContextData().getValue(PowertoolsLoggedFields.FUNCTION_TRACE_ID.getName()); + jsonWriter.writeString(traceId); + } + }; + + private static final EventResolver SERVICE_RESOLVER = + (final LogEvent logEvent, final JsonWriter jsonWriter) -> { + final String service = logEvent.getContextData().getValue(PowertoolsLoggedFields.SERVICE.getName()); + jsonWriter.writeString(service); + }; + + private static final EventResolver REGION_RESOLVER = + (final LogEvent logEvent, final JsonWriter jsonWriter) -> + jsonWriter.writeString(SystemWrapper.getenv(LambdaConstants.AWS_REGION_ENV)); + + public static final String LAMBDA_ARN_REGEX = + "^arn:(aws|aws-us-gov|aws-cn):lambda:[a-zA-Z0-9-]+:\\d{12}:function:[a-zA-Z0-9-_]+(:[a-zA-Z0-9-_]+)?$"; + + private static final EventResolver ACCOUNT_ID_RESOLVER = new EventResolver() { + @Override + public boolean isResolvable(LogEvent logEvent) { + final String arn = logEvent.getContextData().getValue(PowertoolsLoggedFields.FUNCTION_ARN.getName()); + return null != arn && !arn.isEmpty() && arn.matches(LAMBDA_ARN_REGEX); + } + + @Override + public void resolve(LogEvent logEvent, JsonWriter jsonWriter) { + final String arn = logEvent.getContextData().getValue(PowertoolsLoggedFields.FUNCTION_ARN.getName()); + jsonWriter.writeString(arn.split(":")[4]); + } + }; + + /** + * 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); + } + } + } + + 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); + } + }); + }; + + private final EventResolver internalResolver; + + private static final Map eventResolverMap = Stream.of(new Object[][] { + { SERVICE.getName(), SERVICE_RESOLVER }, + { FUNCTION_NAME.getName(), FUNCTION_NAME_RESOLVER }, + { FUNCTION_VERSION.getName(), FUNCTION_VERSION_RESOLVER }, + { FUNCTION_ARN.getName(), FUNCTION_ARN_RESOLVER }, + { FUNCTION_MEMORY_SIZE.getName(), FUNCTION_MEMORY_RESOLVER }, + { FUNCTION_REQUEST_ID.getName(), FUNCTION_REQ_RESOLVER }, + { FUNCTION_COLD_START.getName(), COLD_START_RESOLVER }, + { FUNCTION_TRACE_ID.getName(), XRAY_TRACE_RESOLVER }, + { 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])); + + + PowertoolsResolver(final TemplateResolverConfig config) { + final String fieldName = config.getString("field"); + 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); + } + if (internalResolver == null) { + throw new IllegalArgumentException("unknown field: " + fieldName); + } + } + } + + @Override + public void resolve(LogEvent value, JsonWriter jsonWriter) { + internalResolver.resolve(value, jsonWriter); + } + + @Override + public boolean isResolvable(LogEvent value) { + return value != null && value.getContextData() != null && internalResolver.isResolvable(value); + } +} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsResolverFactory.java b/powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolverFactory.java similarity index 71% rename from powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsResolverFactory.java rename to powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolverFactory.java index 7d688f469..297c00691 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsResolverFactory.java +++ b/powertools-logging/powertools-logging-log4j/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolverFactory.java @@ -12,21 +12,20 @@ * */ -package software.amazon.lambda.powertools.logging.internal; +package org.apache.logging.log4j.layout.template.json.resolver; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.config.plugins.Plugin; import org.apache.logging.log4j.core.config.plugins.PluginFactory; -import org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext; -import org.apache.logging.log4j.layout.template.json.resolver.EventResolverFactory; -import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolver; -import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverConfig; -import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverFactory; +/** + * Factory for {@link PowertoolsResolver}. Log4j plugin to process powertools fields in the layout.json + */ @Plugin(name = "PowertoolsResolverFactory", category = TemplateResolverFactory.CATEGORY) public final class PowertoolsResolverFactory implements EventResolverFactory { private static final PowertoolsResolverFactory INSTANCE = new PowertoolsResolverFactory(); + private static final String RESOLVER_NAME = "powertools"; private PowertoolsResolverFactory() { } @@ -38,12 +37,12 @@ public static PowertoolsResolverFactory getInstance() { @Override public String getName() { - return PowertoolsResolver.getName(); + return RESOLVER_NAME; } @Override public TemplateResolver create(EventResolverContext context, TemplateResolverConfig config) { - return new PowertoolsResolver(); + return new PowertoolsResolver(config); } } diff --git a/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4/internal/Log4jLoggingManager.java b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4/internal/Log4jLoggingManager.java new file mode 100644 index 000000000..4e57a8e45 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/main/java/software/amazon/lambda/powertools/logging/log4/internal/Log4jLoggingManager.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.log4.internal; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.Configurator; +import org.slf4j.Logger; +import software.amazon.lambda.powertools.logging.internal.LoggingManager; + +/** + * LoggingManager for Log4j2 (see {@link LoggingManager}). + */ +public class Log4jLoggingManager implements LoggingManager { + + /** + * @inheritDoc + */ + @Override + @SuppressWarnings("java:S4792") + public void setLogLevel(org.slf4j.event.Level logLevel) { + LoggerContext ctx = (LoggerContext) LogManager.getContext(false); + Configurator.setAllLevels(LogManager.getRootLogger().getName(), Level.getLevel(logLevel.toString())); + ctx.updateLoggers(); + } + + /** + * @inheritDoc + */ + @Override + public org.slf4j.event.Level getLogLevel(Logger logger) { + LoggerContext ctx = (LoggerContext) LogManager.getContext(false); + return org.slf4j.event.Level.valueOf(ctx.getLogger(logger.getName()).getLevel().toString()); + } +} diff --git a/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaEcsLayout.json b/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaEcsLayout.json new file mode 100644 index 000000000..19f13f199 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaEcsLayout.json @@ -0,0 +1,89 @@ +{ + "@timestamp": { + "$resolver": "timestamp", + "pattern": { + "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", + "timeZone": "UTC" + } + }, + "ecs.version": "1.2.0", + "log.level": { + "$resolver": "level", + "field": "name" + }, + "message": { + "$resolver": "message" + }, + "error.type": { + "$resolver": "exception", + "field": "className" + }, + "error.message": { + "$resolver": "exception", + "field": "message" + }, + "error.stack_trace": { + "$resolver": "exception", + "field": "stackTrace", + "stackTrace": { + "stringified": true + } + }, + "service.name": { + "$resolver": "powertools", + "field": "service" + }, + "service.version": { + "$resolver": "powertools", + "field": "function_version" + }, + "log.logger": { + "$resolver": "logger", + "field": "name" + }, + "process.thread.name": { + "$resolver": "thread", + "field": "name" + }, + "cloud.provider" : "aws", + "cloud.service.name" : "lambda", + "cloud.region" : { + "$resolver": "powertools", + "field": "region" + }, + "cloud.account.id" : { + "$resolver": "powertools", + "field": "account_id" + }, + "faas.id": { + "$resolver": "powertools", + "field": "function_arn" + }, + "faas.name": { + "$resolver": "powertools", + "field": "function_name" + }, + "faas.version": { + "$resolver": "powertools", + "field": "function_version" + }, + "faas.memory": { + "$resolver": "powertools", + "field": "function_memory_size" + }, + "faas.execution": { + "$resolver": "powertools", + "field": "function_request_id" + }, + "faas.coldstart": { + "$resolver": "powertools", + "field": "cold_start" + }, + "trace.id": { + "$resolver": "powertools", + "field": "xray_trace_id" + }, + "": { + "$resolver": "powertools" + } +} \ No newline at end of file diff --git a/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaJsonLayout.json b/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaJsonLayout.json new file mode 100644 index 000000000..d8d8810f6 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/main/resources/LambdaJsonLayout.json @@ -0,0 +1,72 @@ +{ + "level": { + "$resolver": "level", + "field": "name" + }, + "message": { + "$resolver": "powertools", + "field": "message" + }, + "error": { + "message": { + "$resolver": "exception", + "field": "message" + }, + "name": { + "$resolver": "exception", + "field": "className" + }, + "stack": { + "$resolver": "exception", + "field": "stackTrace", + "stackTrace": { + "stringified": true + } + } + }, + "cold_start": { + "$resolver": "powertools", + "field": "cold_start" + }, + "function_arn": { + "$resolver": "powertools", + "field": "function_arn" + }, + "function_memory_size": { + "$resolver": "powertools", + "field": "function_memory_size" + }, + "function_name": { + "$resolver": "powertools", + "field": "function_name" + }, + "function_request_id": { + "$resolver": "powertools", + "field": "function_request_id" + }, + "function_version": { + "$resolver": "powertools", + "field": "function_version" + }, + "sampling_rate": { + "$resolver": "powertools", + "field": "sampling_rate" + }, + "service": { + "$resolver": "powertools", + "field": "service" + }, + "timestamp": { + "$resolver": "timestamp", + "pattern": { + "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + }, + "xray_trace_id": { + "$resolver": "powertools", + "field": "xray_trace_id" + }, + "": { + "$resolver": "powertools" + } +} diff --git a/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager b/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager new file mode 100644 index 000000000..d444c5525 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/main/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager @@ -0,0 +1 @@ +software.amazon.lambda.powertools.logging.log4.internal.Log4jLoggingManager \ No newline at end of file diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowerToolsResolverFactoryTest.java b/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowerToolsResolverFactoryTest.java new file mode 100644 index 000000000..4cf798a47 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowerToolsResolverFactoryTest.java @@ -0,0 +1,95 @@ +/* + * 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 org.apache.logging.log4j.layout.template.json.resolver; + +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 com.amazonaws.services.lambda.runtime.Context; +import java.io.File; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.file.NoSuchFileException; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.slf4j.MDC; +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; +import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEnabled; + +@Order(1) +class PowerToolsResolverFactoryTest { + + @Mock + private Context context; + + @BeforeEach + void setUp() throws IllegalAccessException, IOException { + openMocks(this); + MDC.clear(); + writeStaticField(LambdaHandlerProcessor.class, "IS_COLD_START", null, true); + setupContext(); + // Make sure file is cleaned up before running tests + try { + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + FileChannel.open(Paths.get("target/ecslogfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } catch (NoSuchFileException e) { + // file may not exist on the first launch + } + } + + @AfterEach + void cleanUp() throws IOException{ + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + FileChannel.open(Paths.get("target/ecslogfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } + + @Test + void shouldLogInJsonFormat() { + PowertoolsLogEnabled handler = new PowertoolsLogEnabled(); + handler.handleRequest("Input", context); + + 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\",\"service\":\"testLog4j\",\"timestamp\":") + .contains("\"xray_trace_id\":\"1-63441c4a-abcdef012345678912345678\",\"myKey\":\"myValue\"}\n"); + } + + @Test + void shouldLogInEcsFormat() { + PowertoolsLogEnabled handler = new PowertoolsLogEnabled(); + handler.handleRequest("Input", context); + + File logFile = new File("target/ecslogfile.json"); + assertThat(contentOf(logFile)).contains( + "\"ecs.version\":\"1.2.0\",\"log.level\":\"DEBUG\",\"message\":\"Test debug event\",\"service.name\":\"testLog4j\",\"service.version\":\"1\",\"log.logger\":\"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEnabled\",\"process.thread.name\":\"main\",\"cloud.provider\":\"aws\",\"cloud.service.name\":\"lambda\",\"cloud.region\":\"eu-central-1\",\"cloud.account.id\":\"012345678910\",\"faas.id\":\"arn:aws:lambda:eu-west-1:012345678910:function:testFunction:1\",\"faas.name\":\"testFunction\",\"faas.version\":\"1\",\"faas.memory\":1024,\"faas.execution\":\"RequestId\",\"faas.coldstart\":true,\"trace.id\":\"1-63441c4a-abcdef012345678912345678\",\"myKey\":\"myValue\"}\n"); + } + + private void setupContext() { + when(context.getFunctionName()).thenReturn("testFunction"); + when(context.getInvokedFunctionArn()).thenReturn( + "arn:aws:lambda:eu-west-1:012345678910:function:testFunction:1"); + when(context.getFunctionVersion()).thenReturn("1"); + when(context.getMemoryLimitInMB()).thenReturn(1024); + when(context.getAwsRequestId()).thenReturn("RequestId"); + } +} 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/PowertoolsMessageResolverTest.java new file mode 100644 index 000000000..a00b78906 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsMessageResolverTest.java @@ -0,0 +1,115 @@ +/* + * 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 org.apache.logging.log4j.layout.template.json.resolver; + +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 com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import java.io.File; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.file.NoSuchFileException; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Order; +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; + +@Order(2) +class PowertoolsMessageResolverTest { + + @Mock + private Context context; + + @BeforeEach + void setUp() throws IOException { + openMocks(this); + MDC.clear(); + setupContext(); + + try { + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + FileChannel.open(Paths.get("target/ecslogfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } catch (NoSuchFileException e) { + // may not be there in the first run + } + } + + @AfterEach + void cleanUp() throws IOException { + //Make sure file is cleaned up before running full stack logging regression + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + FileChannel.open(Paths.get("target/ecslogfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } + + @Test + void shouldLogJsonMessageWithoutEscapedStringsWhenSettingLogAsJson() { + // GIVEN + PowertoolsJsonMessage requestHandler = new PowertoolsJsonMessage(); + 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("\"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("\"message\":\"1212abcd\"") + .contains("\"message\":\"{\\\"key\\\":\\\"value\\\"}\"") + .contains("\"message\":\"Message body = plop and id = \\\"1212abcd\\\"\"") + .doesNotContain(LoggingUtils.LOG_MESSAGES_AS_JSON); + } + + @Test + void shouldLogStringMessageWhenNotJson() { + // GIVEN + PowertoolsLogEnabled requestHandler = new PowertoolsLogEnabled(); + + // WHEN + requestHandler.handleRequest(null, context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).contains("\"message\":\"Test debug event\""); + } + + private void setupContext() { + when(context.getFunctionName()).thenReturn("testFunction"); + when(context.getInvokedFunctionArn()).thenReturn("testArn"); + when(context.getFunctionVersion()).thenReturn("1"); + when(context.getMemoryLimitInMB()).thenReturn(10); + when(context.getAwsRequestId()).thenReturn("RequestId"); + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..1aa98fdef --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PowertoolsResolverTest.java @@ -0,0 +1,110 @@ +/* + * 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 org.apache.logging.log4j.layout.template.json.resolver; + +import static org.assertj.core.api.Assertions.assertThat; +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; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_COLD_START; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_MEMORY_SIZE; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.SAMPLING_RATE; + +import java.util.HashMap; +import java.util.Map; +import org.apache.logging.log4j.core.impl.Log4jLogEvent; +import org.apache.logging.log4j.layout.template.json.util.JsonWriter; +import org.apache.logging.log4j.util.SortedArrayStringMap; +import org.apache.logging.log4j.util.StringMap; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.MockedStatic; +import software.amazon.lambda.powertools.common.internal.SystemWrapper; +import software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields; + +class PowertoolsResolverTest { + + @ParameterizedTest + @EnumSource(value = PowertoolsLoggedFields.class, + mode = EnumSource.Mode.EXCLUDE, + names = {"FUNCTION_MEMORY_SIZE", "SAMPLING_RATE", "FUNCTION_COLD_START", "CORRELATION_ID"}) + void shouldResolveFunctionStringInfo(PowertoolsLoggedFields field) { + String result = resolveField(field.getName(), "value"); + assertThat(result).isEqualTo("\"value\""); + } + + @Test + void shouldResolveMemorySize() { + String result = resolveField(FUNCTION_MEMORY_SIZE.getName(), "42"); + assertThat(result).isEqualTo("42"); + } + + @Test + void shouldResolveSamplingRate() { + String result = resolveField(SAMPLING_RATE.getName(), "0.4"); + assertThat(result).isEqualTo("0.4"); + } + + @Test + void shouldResolveColdStart() { + String result = resolveField(FUNCTION_COLD_START.getName(), "true"); + assertThat(result).isEqualTo("true"); + } + + @Test + void shouldResolveAccountId() { + String result = resolveField(FUNCTION_ARN.getName(), "account_id", "arn:aws:lambda:us-east-2:123456789012:function:my-function"); + assertThat(result).isEqualTo("\"123456789012\""); + } + + @Test + void shouldResolveRegion() { + try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { + mocked.when(() -> getenv("AWS_REGION")) + .thenReturn("eu-central-2"); + + String result = resolveField("region", "dummy, will use the env var"); + assertThat(result).isEqualTo("\"eu-central-2\""); + } + } + + private static String resolveField(String field, String value) { + return resolveField(field, field, value); + } + + private static String resolveField(String data, String field, String value) { + Map configMap = new HashMap<>(); + configMap.put("field", field); + + TemplateResolverConfig config = new TemplateResolverConfig(configMap); + PowertoolsResolver resolver = new PowertoolsResolver(config); + JsonWriter writer = JsonWriter + .newBuilder() + .setMaxStringLength(1000) + .setTruncatedStringSuffix("") + .build(); + + StringMap contextMap = new SortedArrayStringMap(); + contextMap.putValue(data, value); + + Log4jLogEvent logEvent = Log4jLogEvent.newBuilder().setContextData(contextMap).build(); + if (resolver.isResolvable(logEvent)) { + resolver.resolve(logEvent, writer); + } + + return writer.getStringBuilder().toString(); + } +} diff --git a/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/Log4jLoggingManagerTest.java b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/Log4jLoggingManagerTest.java new file mode 100644 index 000000000..69e1ee710 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/Log4jLoggingManagerTest.java @@ -0,0 +1,51 @@ +package software.amazon.lambda.powertools.logging.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.slf4j.event.Level.DEBUG; +import static org.slf4j.event.Level.ERROR; +import static org.slf4j.event.Level.WARN; + +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; +import software.amazon.lambda.powertools.logging.log4.internal.Log4jLoggingManager; + +class Log4jLoggingManagerTest { + + private final static Logger LOG = LoggerFactory.getLogger(Log4jLoggingManagerTest.class); + private final static Logger ROOT = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + + @Test + @Order(1) + void getLogLevel_shouldReturnConfiguredLogLevel() { + // Given log4j2.xml in resources + + // When + Log4jLoggingManager manager = new Log4jLoggingManager(); + Level logLevel = manager.getLogLevel(LOG); + Level rootLevel = manager.getLogLevel(ROOT); + + // Then + assertThat(logLevel).isEqualTo(DEBUG); + assertThat(rootLevel).isEqualTo(WARN); + } + + @Test + @Order(2) + void resetLogLevel() { + // Given log4j2.xml in resources + + // When + Log4jLoggingManager manager = new Log4jLoggingManager(); + manager.setLogLevel(ERROR); + + Level rootLevel = manager.getLogLevel(ROOT); + Level logLevel = manager.getLogLevel(LOG); + + // Then + assertThat(rootLevel).isEqualTo(ERROR); + assertThat(logLevel).isEqualTo(ERROR); + } +} 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/PowertoolsJsonMessage.java new file mode 100644 index 000000000..3d196e5fb --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsJsonMessage.java @@ -0,0 +1,47 @@ +/* + * 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 com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.logging.LoggingUtils; +import software.amazon.lambda.powertools.utilities.JsonConfig; + +public class PowertoolsJsonMessage implements RequestHandler { + private final Logger LOG = LoggerFactory.getLogger(PowertoolsJsonMessage.class); + + @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\"}"); + + LOG.debug("{}", input.getMessageId()); + LOG.warn("Message body = {} and id = \"{}\"", input.getBody(), input.getMessageId()); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + return input.getMessageId(); + } +} 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 new file mode 100644 index 000000000..faa722756 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEnabled.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.RequestHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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); + + @Override + @Logging(clearState = true) + public Object handleRequest(Object input, Context context) { + LoggingUtils.appendKey("myKey", "myValue"); + LOG.debug("Test debug event"); + return null; + } +} diff --git a/powertools-logging/powertools-logging-log4j/src/test/resources/junit-platform.properties b/powertools-logging/powertools-logging-log4j/src/test/resources/junit-platform.properties new file mode 100644 index 000000000..80a2481d7 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/test/resources/junit-platform.properties @@ -0,0 +1,17 @@ +# +# 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. +# +# + +# because of LambdaLoggingAspect static initialization of the LoggingManager, we need to +# set an order in the unit tests, especially LambdaLoggingAspectTest needs to be first +junit.jupiter.testclass.order.default=org.junit.jupiter.api.ClassOrderer$OrderAnnotation \ No newline at end of file diff --git a/powertools-logging/powertools-logging-log4j/src/test/resources/log4j2.xml b/powertools-logging/powertools-logging-log4j/src/test/resources/log4j2.xml new file mode 100644 index 000000000..778077bc5 --- /dev/null +++ b/powertools-logging/powertools-logging-log4j/src/test/resources/log4j2.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + q + + + + + + + + + + \ No newline at end of file diff --git a/powertools-logging/powertools-logging-logback/pom.xml b/powertools-logging/powertools-logging-logback/pom.xml new file mode 100644 index 000000000..99fff3ab9 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/pom.xml @@ -0,0 +1,137 @@ + + + 4.0.0 + + powertools-parent + software.amazon.lambda + 2.0.0-SNAPSHOT + ../../pom.xml + + + powertools-logging-logback + Powertools for AWS Lambda (Java) - Logging with LogBack + Set of utility for better logging with logback + + + + software.amazon.lambda + powertools-logging + ${project.version} + + + org.aspectj + aspectjrt + provided + + + ch.qos.logback + logback-classic + + 1.3.4 + provided + + + com.sun.mail + javax.mail + + + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.apache.commons + commons-lang3 + test + + + org.aspectj + aspectjweaver + test + + + org.assertj + assertj-core + test + + + com.amazonaws + aws-lambda-java-events + test + + + com.amazonaws + aws-lambda-java-tests + test + + + org.skyscreamer + jsonassert + test + + + + + + + dev.aspectj + aspectj-maven-plugin + 1.13.1 + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-logging + + + + + + + compile + + + + + + org.aspectj + aspectjtools + ${aspectj.version} + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + + testLogback + eu-central-1 + <_X_AMZN_TRACE_ID>Root=1-63441c4a-abcdef012345678912345678 + + + + + + + + \ No newline at end of file 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 new file mode 100644 index 000000000..1fc98ec67 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaEcsEncoder.java @@ -0,0 +1,199 @@ +/* + * 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 static java.nio.charset.StandardCharsets.UTF_8; +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; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_NAME; +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 ch.qos.logback.classic.pattern.ThrowableHandlingConverter; +import ch.qos.logback.classic.pattern.ThrowableProxyConverter; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.IThrowableProxy; +import ch.qos.logback.classic.spi.ThrowableProxy; +import ch.qos.logback.core.encoder.EncoderBase; +import java.util.Map; +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; +import software.amazon.lambda.powertools.logging.logback.internal.LambdaEcsSerializer; + + +/** + * This class will encode the logback event into the format expected by the Elastic Common Schema (ECS) service (for Elasticsearch). + *
+ * Inspired from co.elastic.logging.logback.EcsEncoder, this class doesn't use + * any JSON (de)serialization library (Jackson, Gson, etc.) or Elastic library to avoid the dependency. + *
+ * This encoder also adds cloud information (see doc) + * and Lambda function information (see doc, currently in beta). + */ +public class LambdaEcsEncoder extends EncoderBase { + + protected static final String ECS_VERSION = "1.2.0"; + protected static final String CLOUD_PROVIDER = "aws"; + protected static final String CLOUD_SERVICE = "lambda"; + + private final ThrowableProxyConverter throwableProxyConverter = new ThrowableProxyConverter(); + protected ThrowableHandlingConverter throwableConverter = null; + private boolean includeCloudInfo = true; + private boolean includeFaasInfo = true; + + @Override + public byte[] headerBytes() { + return null; + } + + /** + * Main method of the encoder. Encode a logging event into Json format (with Elastic Search fields) + * + * @param event the logging event + * @return the encoded bytes + */ + @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)); + } + } + 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()); + + if (includeCloudInfo) { + LambdaEcsSerializer.serializeCloudProvider(builder, CLOUD_PROVIDER); + LambdaEcsSerializer.serializeCloudService(builder, CLOUD_SERVICE); + if (arn != null) { + String[] arnParts = arn.split(":"); + LambdaEcsSerializer.serializeCloudRegion(builder, arnParts[3]); + LambdaEcsSerializer.serializeCloudAccountId(builder, 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())); + } + LambdaEcsSerializer.serializeAdditionalFields(builder, event.getMDCPropertyMap()); + LambdaEcsSerializer.serializeObjectEnd(builder); + return builder.toString().getBytes(UTF_8); + } + + @Override + public byte[] footerBytes() { + return null; + } + + /** + * Specify a throwable converter to format the stacktrace according to your need + * (default is null, no throwableConverter): + *
+ *
{@code
+     *     
+     *         
+     *              30
+     *              2048
+     *              20
+     *              sun\.reflect\..*\.invoke.*
+     *              net\.sf\.cglib\.proxy\.MethodProxy\.invoke
+     *              
+     *              true
+     *              true
+     *         
+     *     
+     * }
+ * + * @param throwableConverter converter for the throwable + */ + public void setThrowableConverter(ThrowableHandlingConverter throwableConverter) { + this.throwableConverter = throwableConverter; + } + + /** + * Specify if cloud information should be logged (default is true): + *
    + *
  • cloud.provider
  • + *
  • cloud.service.name
  • + *
  • cloud.region
  • + *
  • cloud.account.id
  • + *
+ *
+ * We strongly recommend to keep these information. + *
+ *
{@code
+     *     
+     *         false
+     *     
+     * }
+ * + * @param includeCloudInfo if thread information should be logged + */ + public void setIncludeCloudInfo(boolean includeCloudInfo) { + this.includeCloudInfo = includeCloudInfo; + } + + /** + * Specify if Lambda function information should be logged (default is true): + *
    + *
  • faas.id
  • + *
  • faas.name
  • + *
  • faas.version
  • + *
  • faas.memory
  • + *
  • faas.execution
  • + *
  • faas.coldstart
  • + *
  • trace.id
  • + *
+ *
+ * We strongly recommend to keep these information. + *
+ *
{@code
+     *     
+     *         false
+     *     
+     * }
+ * + * @param includeFaasInfo if function information should be logged + */ + public void setIncludeFaasInfo(boolean includeFaasInfo) { + this.includeFaasInfo = includeFaasInfo; + } +} 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 new file mode 100644 index 000000000..b951e266e --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/LambdaJsonEncoder.java @@ -0,0 +1,208 @@ +/* + * 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 static java.nio.charset.StandardCharsets.UTF_8; +import static software.amazon.lambda.powertools.logging.LoggingUtils.LOG_MESSAGES_AS_JSON; + +import ch.qos.logback.classic.pattern.ThrowableHandlingConverter; +import ch.qos.logback.classic.pattern.ThrowableProxyConverter; +import ch.qos.logback.classic.spi.ILoggingEvent; +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; + +/** + * 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 { + + 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; + } + + @Override + public void start() { + super.start(); + throwableProxyConverter.start(); + if (throwableConverter != null) { + throwableConverter.start(); + } + } + + @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)); + IThrowableProxy throwableProxy = event.getThrowableProxy(); + if (throwableProxy != null) { + if (throwableConverter != null) { + LambdaJsonSerializer.serializeException(builder, throwableProxy.getClassName(), + throwableProxy.getMessage(), throwableConverter.convert(event)); + } else if (throwableProxy instanceof ThrowableProxy) { + LambdaJsonSerializer.serializeException(builder, ((ThrowableProxy) throwableProxy).getThrowable()); + } else { + LambdaJsonSerializer.serializeException(builder, 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; + } + + /** + * Specify the format of the timestamp (default is yyyy-MM-dd'T'HH:mm:ss.SSS'Z'). + * Note that if you use the Lambda Advanced Logging Configuration, you should keep the default format. + *
+ *
{@code
+     *     
+     *         yyyy-MM-dd'T'HH:mm:ss.SSSZz
+     *     
+     * }
+ * + * @param timestampFormat format of the timestamp (compatible with {@link java.text.SimpleDateFormat}) + */ + public void setTimestampFormat(String timestampFormat) { + this.timestampFormat = timestampFormat; + } + + /** + * Specify the format of the time zone id for timestamp (default is null, no timezone): + *
+ *
{@code
+     *     
+     *         Europe/Paris
+     *     
+     * }
+ * + * @param timestampFormatTimezoneId Zone Id (see {@link java.util.TimeZone}) + */ + public void setTimestampFormatTimezoneId(String timestampFormatTimezoneId) { + this.timestampFormatTimezoneId = timestampFormatTimezoneId; + } + + /** + * Specify a throwable converter to format the stacktrace according to your need + * (default is null, no throwableConverter): + *
+ *
{@code
+     *     
+     *         
+     *              30
+     *              2048
+     *              20
+     *              sun\.reflect\..*\.invoke.*
+     *              net\.sf\.cglib\.proxy\.MethodProxy\.invoke
+     *              
+     *              true
+     *              true
+     *         
+     *     
+     * }
+ * + * @param throwableConverter converter for the throwable + */ + public void setThrowableConverter(ThrowableHandlingConverter throwableConverter) { + this.throwableConverter = throwableConverter; + } + + /** + * Specify if thread information should be logged (default is false) + *
+ *
{@code
+     *     
+     *         true
+     *     
+     * }
+ * + * @param includeThreadInfo if thread information should be logged + */ + public void setIncludeThreadInfo(boolean includeThreadInfo) { + this.includeThreadInfo = includeThreadInfo; + } + + /** + * Specify if Lambda function information should be logged (default is true): + *
    + *
  • function_name
  • + *
  • function_version
  • + *
  • function_arn
  • + *
  • function_memory_size
  • + *
  • function_request_id
  • + *
  • cold_start
  • + *
  • xray_trace_id
  • + *
  • sampling_rate
  • + *
  • service
  • + *
+ *
+ * We strongly recommend to keep these information. + *
+ *
{@code
+     *     
+     *         false
+     *     
+     * }
+ * + * @param includePowertoolsInfo if function information should be logged + */ + 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; + } +} 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 new file mode 100644 index 000000000..e604d10c7 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/JsonUtils.java @@ -0,0 +1,130 @@ +/* + * 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 new file mode 100644 index 000000000..bab1a32fc --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/LambdaEcsSerializer.java @@ -0,0 +1,187 @@ +/* + * 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 new file mode 100644 index 000000000..7d7b8d0d7 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/LambdaJsonSerializer.java @@ -0,0 +1,150 @@ +/* + * 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/main/java/software/amazon/lambda/powertools/logging/logback/internal/LogbackLoggingManager.java b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/LogbackLoggingManager.java new file mode 100644 index 000000000..86982b444 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/main/java/software/amazon/lambda/powertools/logging/logback/internal/LogbackLoggingManager.java @@ -0,0 +1,59 @@ +/* + * 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 ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import java.util.List; +import org.slf4j.ILoggerFactory; +import org.slf4j.LoggerFactory; +import software.amazon.lambda.powertools.logging.internal.LoggingManager; + +/** + * LoggingManager for Logback (see {@link LoggingManager}). + */ +public class LogbackLoggingManager implements LoggingManager { + + private final LoggerContext loggerContext; + + public LogbackLoggingManager() { + ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory(); + if (!(loggerFactory instanceof LoggerContext)) { + throw new RuntimeException("LoggerFactory does not match required type: " + LoggerContext.class.getName()); + } + loggerContext = (LoggerContext) loggerFactory; + } + + /** + * @inheritDoc + */ + @Override + @SuppressWarnings("java:S4792") + public void setLogLevel(org.slf4j.event.Level logLevel) { + List loggers = loggerContext.getLoggerList(); + for (Logger logger : loggers) { + logger.setLevel(Level.convertAnSLF4JLevel(logLevel)); + } + } + + /** + * @inheritDoc + */ + @Override + public org.slf4j.event.Level getLogLevel(org.slf4j.Logger logger) { + return org.slf4j.event.Level.valueOf(loggerContext.getLogger(logger.getName()).getEffectiveLevel().toString()); + } +} diff --git a/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager b/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager new file mode 100644 index 000000000..958f2e459 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/main/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager @@ -0,0 +1 @@ +software.amazon.lambda.powertools.logging.logback.internal.LogbackLoggingManager \ No newline at end of file diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java new file mode 100644 index 000000000..214057917 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/LogbackLoggingManagerTest.java @@ -0,0 +1,54 @@ +/* + * 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 static org.slf4j.event.Level.DEBUG; +import static org.slf4j.event.Level.ERROR; +import static org.slf4j.event.Level.WARN; + +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; +import software.amazon.lambda.powertools.logging.logback.internal.LogbackLoggingManager; + +class LogbackLoggingManagerTest { + + private static final Logger LOG = LoggerFactory.getLogger(LogbackLoggingManagerTest.class); + private static final Logger ROOT = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + + @Test + @Order(1) + void getLogLevel_shouldReturnConfiguredLogLevel() { + LogbackLoggingManager manager = new LogbackLoggingManager(); + Level logLevel = manager.getLogLevel(LOG); + assertThat(logLevel).isEqualTo(DEBUG); + + logLevel = manager.getLogLevel(ROOT); + assertThat(logLevel).isEqualTo(WARN); + } + + @Test + @Order(2) + void resetLogLevel() { + LogbackLoggingManager manager = new LogbackLoggingManager(); + manager.setLogLevel(ERROR); + + Level logLevel = manager.getLogLevel(LOG); + assertThat(logLevel).isEqualTo(ERROR); + } +} diff --git a/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaEcsEncoderTest.java b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaEcsEncoderTest.java new file mode 100644 index 000000000..5dcca2fb2 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaEcsEncoderTest.java @@ -0,0 +1,176 @@ +/* + * 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.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 ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.pattern.RootCauseFirstThrowableProxyConverter; +import ch.qos.logback.classic.spi.LoggingEvent; +import com.amazonaws.services.lambda.runtime.Context; +import java.io.File; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.NoSuchFileException; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; +import software.amazon.lambda.powertools.logging.logback.LambdaEcsEncoder; +import software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEnabled; + +@Order(3) +class LambdaEcsEncoderTest { + + private static final Logger logger = (Logger) LoggerFactory.getLogger(LambdaEcsEncoderTest.class.getName()); + + + @Mock + private Context context; + + @BeforeEach + void setUp() throws IllegalAccessException, IOException { + openMocks(this); + MDC.clear(); + writeStaticField(LambdaHandlerProcessor.class, "IS_COLD_START", null, true); + setupContext(); + // Make sure file is cleaned up before running tests + try { + FileChannel.open(Paths.get("target/ecslogfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } catch (NoSuchFileException e) { + // file may not exist on the first launch + } + } + + @AfterEach + void cleanUp() throws IOException{ + FileChannel.open(Paths.get("target/ecslogfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } + + @Test + void shouldLogInEcsFormat() { + PowertoolsLogEnabled handler = new PowertoolsLogEnabled(); + handler.handleRequest("Input", context); + + File logFile = new File("target/ecslogfile.json"); + assertThat(contentOf(logFile)).contains( + "\"ecs.version\":\"1.2.0\",\"log.level\":\"DEBUG\",\"message\":\"Test debug event\",\"service.name\":\"testLogback\",\"service.version\":\"1\",\"log.logger\":\"software.amazon.lambda.powertools.logging.internal.handler.PowertoolsLogEnabled\",\"process.thread.name\":\"main\",\"cloud.provider\":\"aws\",\"cloud.service.name\":\"lambda\",\"cloud.region\":\"eu-west-1\",\"cloud.account.id\":\"012345678910\",\"faas.id\":\"arn:aws:lambda:eu-west-1:012345678910:function:testFunction:1\",\"faas.name\":\"testFunction\",\"faas.version\":\"1\",\"faas.memory\":\"1024\",\"faas.execution\":\"RequestId\",\"faas.coldstart\":\"true\",\"trace.id\":\"1-63441c4a-abcdef012345678912345678\",\"myKey\":\"myValue\"}\n"); + } + + private final LoggingEvent loggingEvent = new LoggingEvent("fqcn", logger, Level.INFO, "message", null, null); + + @Test + void shouldNotLogFunctionInfo() { + // GIVEN + LambdaEcsEncoder encoder = new LambdaEcsEncoder(); + setMDC(); + + // WHEN + byte[] encoded = encoder.encode(loggingEvent); + String result = new String(encoded, StandardCharsets.UTF_8); + + // THEN + assertThat(result).contains("\"faas.id\":\"arn:aws:lambda:eu-west-1:012345678910:function:testFunction:1\",\"faas.name\":\"testFunction\",\"faas.version\":\"1\",\"faas.memory\":\"1024\",\"faas.execution\":\"RequestId\",\"faas.coldstart\":\"false\""); + + // WHEN (includeFaasInfo = false) + encoder.setIncludeFaasInfo(false); + encoded = encoder.encode(loggingEvent); + result = new String(encoded, StandardCharsets.UTF_8); + + // THEN (no faas info in logs) + assertThat(result).doesNotContain("faas"); + } + + @Test + void shouldNotLogCloudInfo() { + // GIVEN + LambdaEcsEncoder encoder = new LambdaEcsEncoder(); + setMDC(); + + // WHEN + byte[] encoded = encoder.encode(loggingEvent); + String result = new String(encoded, StandardCharsets.UTF_8); + + // THEN + assertThat(result).contains("\"cloud.provider\":\"aws\",\"cloud.service.name\":\"lambda\",\"cloud.region\":\"eu-west-1\",\"cloud.account.id\":\"012345678910\""); + + // WHEN (includeCloudInfo = false) + encoder.setIncludeCloudInfo(false); + encoded = encoder.encode(loggingEvent); + result = new String(encoded, StandardCharsets.UTF_8); + + // THEN (no faas info in logs) + assertThat(result).doesNotContain("cloud"); + } + + @Test + void shouldLogException() { + // GIVEN + LambdaEcsEncoder encoder = new LambdaEcsEncoder(); + encoder.start(); + LoggingEvent errorloggingEvent = new LoggingEvent("fqcn", logger, Level.INFO, "Error", new IllegalStateException("Unexpected value"), null); + + // WHEN + byte[] encoded = encoder.encode(errorloggingEvent); + String result = new String(encoded, StandardCharsets.UTF_8); + + // THEN + assertThat(result).contains("\"message\":\"Error\",\"error.message\":\"Unexpected value\",\"error.type\":\"java.lang.IllegalStateException\",\"error.stack_trace\":\"[software.amazon.lambda.powertools.logging.internal.LambdaEcsEncoderTest.shouldLogException"); + + // WHEN (configure a custom throwableConverter) + encoder = new LambdaEcsEncoder(); + RootCauseFirstThrowableProxyConverter throwableConverter = new RootCauseFirstThrowableProxyConverter(); + encoder.setThrowableConverter(throwableConverter); + encoder.start(); + encoded = encoder.encode(errorloggingEvent); + result = new String(encoded, StandardCharsets.UTF_8); + + // THEN (stack is logged with root cause first) + assertThat(result).contains("\"message\":\"Error\",\"error.message\":\"Unexpected value\",\"error.type\":\"java.lang.IllegalStateException\",\"error.stack_trace\":\"java.lang.IllegalStateException: Unexpected value\n"); + } + + private void setMDC() { + MDC.put(PowertoolsLoggedFields.FUNCTION_NAME.getName(), context.getFunctionName()); + MDC.put(PowertoolsLoggedFields.FUNCTION_ARN.getName(), context.getInvokedFunctionArn()); + MDC.put(PowertoolsLoggedFields.FUNCTION_VERSION.getName(), context.getFunctionVersion()); + MDC.put(PowertoolsLoggedFields.FUNCTION_MEMORY_SIZE.getName(), + String.valueOf(context.getMemoryLimitInMB())); + MDC.put(PowertoolsLoggedFields.FUNCTION_REQUEST_ID.getName(), context.getAwsRequestId()); + MDC.put(PowertoolsLoggedFields.FUNCTION_COLD_START.getName(), "false"); + MDC.put(PowertoolsLoggedFields.SAMPLING_RATE.getName(), "0.2"); + MDC.put(PowertoolsLoggedFields.SERVICE.getName(), "Service"); + } + + private void setupContext() { + when(context.getFunctionName()).thenReturn("testFunction"); + when(context.getInvokedFunctionArn()).thenReturn( + "arn:aws:lambda:eu-west-1:012345678910:function:testFunction:1"); + when(context.getFunctionVersion()).thenReturn("1"); + when(context.getMemoryLimitInMB()).thenReturn(1024); + when(context.getAwsRequestId()).thenReturn("RequestId"); + } +} 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 new file mode 100644 index 000000000..dc8ac429b --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonEncoderTest.java @@ -0,0 +1,263 @@ +/* + * 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 com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; +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; +import ch.qos.logback.classic.pattern.RootCauseFirstThrowableProxyConverter; +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 java.io.File; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.NoSuchFileException; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Collections; +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.BeforeEach; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +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.internal.handler.PowertoolsLogEnabled; +import software.amazon.lambda.powertools.utilities.JsonConfig; + +@Order(2) +class LambdaJsonEncoderTest { + private static final Logger logger = (Logger) LoggerFactory.getLogger(LambdaJsonEncoderTest.class.getName()); + + @Mock + private Context context; + + @BeforeAll + private static void init() { + JsonConfig.get().getObjectMapper().setSerializationInclusion(NON_NULL); + } + + @BeforeEach + void setUp() throws IllegalAccessException, IOException { + openMocks(this); + MDC.clear(); + writeStaticField(LambdaHandlerProcessor.class, "IS_COLD_START", null, true); + setupContext(); + // Make sure file is cleaned up before running tests + try { + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } catch (NoSuchFileException e) { + // file may not exist on the first launch + } + } + + @AfterEach + void cleanUp() throws IOException{ + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } + + @Test + void shouldLogInJsonFormat() { + // GIVEN + PowertoolsLogEnabled handler = new PowertoolsLogEnabled(); + + // WHEN + handler.handleRequest("Input", context); + + // 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\":"); + } + + @Test + void shouldLogJsonMessageWithoutEscapedStrings() { + // GIVEN + PowertoolsJsonMessage requestHandler = new PowertoolsJsonMessage(); + 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("\"message\":{\"messageId\":\"1212abcd\",\"body\":\"plop\",\"eventSource\":\"eb\",\"awsRegion\":\"eu-west-1\",\"messageAttributes\":{\"keyAttribute\":{\"stringListValues\":[\"val1\",\"val2\",\"val3\"]}}}") + .contains("\"message\":\"1212abcd\"") + .contains("\"message\":\"Message body = plop and id = \\\"1212abcd\\\"\"") + .doesNotContain(LOG_MESSAGES_AS_JSON); + } + + private final LoggingEvent loggingEvent = new LoggingEvent("fqcn", logger, Level.INFO, "message", null, null); + + @Test + void shouldNotLogPowertoolsInfo() { + // GIVEN + LambdaJsonEncoder encoder = new LambdaJsonEncoder(); + + MDC.put(PowertoolsLoggedFields.FUNCTION_NAME.getName(), context.getFunctionName()); + MDC.put(PowertoolsLoggedFields.FUNCTION_ARN.getName(), context.getInvokedFunctionArn()); + MDC.put(PowertoolsLoggedFields.FUNCTION_VERSION.getName(), context.getFunctionVersion()); + MDC.put(PowertoolsLoggedFields.FUNCTION_MEMORY_SIZE.getName(), + String.valueOf(context.getMemoryLimitInMB())); + MDC.put(PowertoolsLoggedFields.FUNCTION_REQUEST_ID.getName(), context.getAwsRequestId()); + MDC.put(PowertoolsLoggedFields.FUNCTION_COLD_START.getName(), "false"); + MDC.put(PowertoolsLoggedFields.SAMPLING_RATE.getName(), "0.2"); + MDC.put(PowertoolsLoggedFields.SERVICE.getName(), "Service"); + + // WHEN + byte[] encoded = encoder.encode(loggingEvent); + String result = new String(encoded, StandardCharsets.UTF_8); + + // THEN + assertThat(result).contains("{\"level\":\"INFO\",\"message\":\"message\",\"cold_start\":false,\"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,\"sampling_rate\":0.2,\"service\":\"Service\",\"timestamp\":"); + + // WHEN (powertoolsInfo = false) + encoder.setIncludePowertoolsInfo(false); + encoded = encoder.encode(loggingEvent); + result = new String(encoded, StandardCharsets.UTF_8); + + // THEN (no powertools info in logs) + assertThat(result).doesNotContain("cold_start", "function_arn", "function_memory_size", "function_name", "function_request_id", "function_version", "sampling_rate", "service"); + } + + @Test + void shouldLogMessagesAsJsonWhenEnabledInLogbackConfig() throws JsonProcessingException { + // GIVEN + LambdaJsonEncoder encoder = new LambdaJsonEncoder(); + encoder.setLogMessagesAsJson(true); + + 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 + LoggingEvent loggingEvent = new LoggingEvent("fqcn", logger, Level.INFO, JsonConfig.get().getObjectMapper().writeValueAsString(msg), null, null); + 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\"]}}}"); + + // WHEN (disabling logging as json) + encoder.setLogMessagesAsJson(false); + encoded = encoder.encode(loggingEvent); + result = new String(encoded, StandardCharsets.UTF_8); + + // THEN (logged as String) + assertThat(result) + .contains("\"message\":\"{\\\"messageId\\\":\\\"1212abcd\\\",\\\"body\\\":\\\"plop\\\",\\\"eventSource\\\":\\\"eb\\\",\\\"awsRegion\\\":\\\"eu-west-1\\\",\\\"messageAttributes\\\":{\\\"keyAttribute\\\":{\\\"stringListValues\\\":[\\\"val1\\\",\\\"val2\\\",\\\"val3\\\"]}}}\""); + } + + @Test + void shouldLogThreadInfo() { + // GIVEN + LambdaJsonEncoder encoder = new LambdaJsonEncoder(); + encoder.setIncludeThreadInfo(true); + + // WHEN + byte[] encoded = encoder.encode(loggingEvent); + String result = new String(encoded, StandardCharsets.UTF_8); + + // THEN + assertThat(result).contains("\"thread\":\"main\",\"thread_id\":1,\"thread_priority\":5"); + } + + @Test + void shouldLogTimestampDifferently() { + // GIVEN + LambdaJsonEncoder encoder = new LambdaJsonEncoder(); + String pattern = "yyyy-MM-dd_HH"; + String timeZone = "Europe/Paris"; + encoder.setTimestampFormat(pattern); + encoder.setTimestampFormatTimezoneId(timeZone); + + // WHEN + Date date = new Date(); + byte[] encoded = encoder.encode(loggingEvent); + String result = new String(encoded, StandardCharsets.UTF_8); + + // THEN + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern); + simpleDateFormat.setTimeZone(TimeZone.getTimeZone(timeZone)); + assertThat(result).contains("\"timestamp\":\""+simpleDateFormat.format(date)+"\""); + } + + @Test + void shouldLogException() { + // GIVEN + LambdaJsonEncoder encoder = new LambdaJsonEncoder(); + encoder.start(); + LoggingEvent errorloggingEvent = new LoggingEvent("fqcn", logger, Level.INFO, "Error", new IllegalStateException("Unexpected value"), null); + + // WHEN + byte[] encoded = encoder.encode(errorloggingEvent); + 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"); + + // WHEN (configure a custom throwableConverter) + encoder = new LambdaJsonEncoder(); + RootCauseFirstThrowableProxyConverter throwableConverter = new RootCauseFirstThrowableProxyConverter(); + encoder.setThrowableConverter(throwableConverter); + encoder.start(); + encoded = encoder.encode(errorloggingEvent); + 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"); + } + + private void setupContext() { + when(context.getFunctionName()).thenReturn("testFunction"); + when(context.getInvokedFunctionArn()).thenReturn( + "arn:aws:lambda:eu-west-1:012345678910:function:testFunction:1"); + when(context.getFunctionVersion()).thenReturn("1"); + when(context.getMemoryLimitInMB()).thenReturn(1024); + when(context.getAwsRequestId()).thenReturn("RequestId"); + } +} 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/PowertoolsJsonMessage.java new file mode 100644 index 000000000..fdc279319 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsJsonMessage.java @@ -0,0 +1,44 @@ +/* + * 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 com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.logging.LoggingUtils; +import software.amazon.lambda.powertools.utilities.JsonConfig; + +public class PowertoolsJsonMessage implements RequestHandler { + private final Logger LOG = LoggerFactory.getLogger(PowertoolsJsonMessage.class); + + @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)); + LOG.debug("{}", input.getMessageId()); + LOG.warn("Message body = {} and id = \"{}\"", input.getBody(), input.getMessageId()); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + return input.getMessageId(); + } +} 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 new file mode 100644 index 000000000..faa722756 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/test/java/software/amazon/lambda/powertools/logging/internal/handler/PowertoolsLogEnabled.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.RequestHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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); + + @Override + @Logging(clearState = true) + public Object handleRequest(Object input, Context context) { + LoggingUtils.appendKey("myKey", "myValue"); + LOG.debug("Test debug event"); + return null; + } +} diff --git a/powertools-logging/powertools-logging-logback/src/test/resources/junit-platform.properties b/powertools-logging/powertools-logging-logback/src/test/resources/junit-platform.properties new file mode 100644 index 000000000..80a2481d7 --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/test/resources/junit-platform.properties @@ -0,0 +1,17 @@ +# +# 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. +# +# + +# because of LambdaLoggingAspect static initialization of the LoggingManager, we need to +# set an order in the unit tests, especially LambdaLoggingAspectTest needs to be first +junit.jupiter.testclass.order.default=org.junit.jupiter.api.ClassOrderer$OrderAnnotation \ No newline at end of file diff --git a/powertools-logging/powertools-logging-logback/src/test/resources/logback-test.xml b/powertools-logging/powertools-logging-logback/src/test/resources/logback-test.xml new file mode 100644 index 000000000..e118a03de --- /dev/null +++ b/powertools-logging/powertools-logging-logback/src/test/resources/logback-test.xml @@ -0,0 +1,27 @@ + + + + + + + + target/logfile.json + + + + + + target/ecslogfile.json + + + + + + + + + + + + + \ No newline at end of file diff --git a/powertools-logging/spotbugs-exclude.xml b/powertools-logging/spotbugs-exclude.xml new file mode 100644 index 000000000..0437849ae --- /dev/null +++ b/powertools-logging/spotbugs-exclude.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/CorrelationIdPathConstants.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/CorrelationIdPaths.java similarity index 69% rename from powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/CorrelationIdPathConstants.java rename to powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/CorrelationIdPaths.java index ce43c9aa0..6fb38502f 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/CorrelationIdPathConstants.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/CorrelationIdPaths.java @@ -17,21 +17,25 @@ /** * Supported Event types from which Correlation ID can be extracted */ -public class CorrelationIdPathConstants { +public class CorrelationIdPaths { /** * To use when function is expecting API Gateway Rest API Request event */ - public static final String API_GATEWAY_REST = "/requestContext/requestId"; + public static final String API_GATEWAY_REST = "requestContext.requestId"; /** * To use when function is expecting API Gateway HTTP API Request event */ - public static final String API_GATEWAY_HTTP = "/requestContext/requestId"; + public static final String API_GATEWAY_HTTP = "requestContext.requestId"; /** * To use when function is expecting Application Load balancer Request event */ - public static final String APPLICATION_LOAD_BALANCER = "/headers/x-amzn-trace-id"; + public static final String APPLICATION_LOAD_BALANCER = "headers.\"x-amzn-trace-id\""; /** * To use when function is expecting Event Bridge Request event */ - public static final String EVENT_BRIDGE = "/id"; + public static final String EVENT_BRIDGE = "id"; + /** + * To use when function is expecting an AppSync request + */ + public static final String APPSYNC_RESOLVER = "request.headers.\"x-amzn-trace-id\""; } 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 9932eb700..05a9cfe31 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 @@ -34,18 +34,19 @@ * {@code com.amazonaws.services.lambda.runtime.Context}

* *
    - *
  • FunctionName
  • - *
  • FunctionVersion
  • - *
  • InvokedFunctionArn
  • - *
  • MemoryLimitInMB
  • + *
  • function_name
  • + *
  • function_version
  • + *
  • function_arn
  • + *
  • function_memory_size
  • + *
  • function_request_id
  • *
* *

By default {@code Logging} will also create keys for:

* *
    - *
  • coldStart - True if this is the first invocation of this Lambda execution environment; else False
  • + *
  • cold_start - True if this is the first invocation of this Lambda execution environment; else False
  • *
  • service - The value of the 'POWER_TOOLS_SERVICE_NAME' environment variable or 'service_undefined'
  • - *
  • samplingRate - The value of the 'POWERTOOLS_LOGGER_SAMPLE_RATE' environment variable or value of samplingRate field or 0. + *
  • sampling_rate - The value of the 'POWERTOOLS_LOGGER_SAMPLE_RATE' environment variable or value of sampling_rate field or 0. * Valid value is from 0.0 to 1.0. Value outside this range is silently ignored.
  • *
* @@ -56,17 +57,35 @@ *

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)}.

* - *

By default {@code Logging} all debug logs will follow log4j2 configuration unless configured via - * POWERTOOLS_LOGGER_SAMPLE_RATE environment variable {@code @Logging(samplingRate = <0.0-1.0>)}.

- * *

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

*/ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Logging { + /** + * Set to true if you want to log the event received by the Lambda function handler.
+ * Can also be configured with the 'POWERTOOLS_LOGGER_LOG_EVENT' environment variable + */ boolean logEvent() default false; + /** + * Set to true if you want to log the response sent by the Lambda function handler.
+ * Can also be configured with the 'POWERTOOLS_LOGGER_LOG_RESPONE' environment variable + */ + boolean logResponse() default false; + + /** + * Set to true if you want to log the exception thrown by the Lambda function handler. + * It is already logged by AWS Lambda but with no context information. Setting this to true + * will log the exception and all the powertools additional fields, for more context.
+ * Can also be configured with the 'POWERTOOLS_LOGGER_LOG_ERROR' environment variable + */ + boolean logError() default false; + + /** + * Sampling rate to change log level to DEBUG. (values must be >=0.0, <=1.0) + */ double samplingRate() default 0; /** 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 index 6e11573cc..d7ceb8ccd 100644 --- 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 @@ -14,18 +14,22 @@ package software.amazon.lambda.powertools.logging; -import static java.util.Arrays.asList; +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.apache.logging.log4j.ThreadContext; +import org.slf4j.MDC; +import software.amazon.lambda.powertools.utilities.JsonConfig; /** - * A class of helper functions to add additional functionality to Logging. - *

- * {@see Logging} + * 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() { @@ -39,7 +43,7 @@ private LoggingUtils() { * @param value The value to be logged */ public static void appendKey(String key, String value) { - ThreadContext.put(key, value); + MDC.put(key, value); } @@ -50,7 +54,7 @@ public static void appendKey(String key, String value) { * @param customKeys Map of custom keys values to be appended to logs */ public static void appendKeys(Map customKeys) { - ThreadContext.putAll(customKeys); + customKeys.forEach(MDC::put); } /** @@ -59,7 +63,7 @@ public static void appendKeys(Map customKeys) { * @param customKey The name of the key to be logged */ public static void removeKey(String customKey) { - ThreadContext.remove(customKey); + MDC.remove(customKey); } @@ -69,7 +73,7 @@ public static void removeKey(String customKey) { * @param keys Map of custom keys values to be appended to logs */ public static void removeKeys(String... keys) { - ThreadContext.removeAll(asList(keys)); + Arrays.stream(keys).forEach(MDC::remove); } /** @@ -78,24 +82,42 @@ public static void removeKeys(String... keys) { * @param value The value of the correlation id */ public static void setCorrelationId(String value) { - ThreadContext.put("correlation_id", 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)}. + * {@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 defaultObjectMapper(ObjectMapper objectMapper) { + public static void setObjectMapper(ObjectMapper objectMapper) { LoggingUtils.objectMapper = objectMapper; } - public static ObjectMapper objectMapper() { - if (null == objectMapper) { - objectMapper = new ObjectMapper(); + public static ObjectMapper getObjectMapper() { + if (LoggingUtils.objectMapper == null) { + LoggingUtils.objectMapper = JsonConfig.get().getObjectMapper(); } - - return objectMapper; + return LoggingUtils.objectMapper; } } diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/AbstractJacksonLayoutCopy.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/AbstractJacksonLayoutCopy.java deleted file mode 100644 index 17d09729f..000000000 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/AbstractJacksonLayoutCopy.java +++ /dev/null @@ -1,519 +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.internal; - -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonRootName; -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import com.fasterxml.jackson.core.JsonGenerationException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectWriter; -import java.io.IOException; -import java.io.Writer; -import java.nio.charset.Charset; -import java.util.LinkedHashMap; -import java.util.Map; -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.Marker; -import org.apache.logging.log4j.ThreadContext; -import org.apache.logging.log4j.core.LogEvent; -import org.apache.logging.log4j.core.config.Configuration; -import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; -import org.apache.logging.log4j.core.config.plugins.PluginElement; -import org.apache.logging.log4j.core.impl.Log4jLogEvent; -import org.apache.logging.log4j.core.impl.ThrowableProxy; -import org.apache.logging.log4j.core.jackson.XmlConstants; -import org.apache.logging.log4j.core.layout.AbstractStringLayout; -import org.apache.logging.log4j.core.lookup.StrSubstitutor; -import org.apache.logging.log4j.core.time.Instant; -import org.apache.logging.log4j.core.util.KeyValuePair; -import org.apache.logging.log4j.core.util.StringBuilderWriter; -import org.apache.logging.log4j.message.Message; -import org.apache.logging.log4j.util.ReadOnlyStringMap; -import org.apache.logging.log4j.util.Strings; - -@Deprecated -abstract class AbstractJacksonLayoutCopy extends AbstractStringLayout { - - protected static final String DEFAULT_EOL = "\r\n"; - protected static final String COMPACT_EOL = Strings.EMPTY; - protected final String eol; - protected final ObjectWriter objectWriter; - protected final boolean compact; - protected final boolean complete; - protected final boolean includeNullDelimiter; - protected final ResolvableKeyValuePair[] additionalFields; - - @Deprecated - protected AbstractJacksonLayoutCopy(final Configuration config, final ObjectWriter objectWriter, - final Charset charset, - final boolean compact, final boolean complete, final boolean eventEol, - final Serializer headerSerializer, - final Serializer footerSerializer) { - this(config, objectWriter, charset, compact, complete, eventEol, headerSerializer, footerSerializer, false); - } - - @Deprecated - protected AbstractJacksonLayoutCopy(final Configuration config, final ObjectWriter objectWriter, - final Charset charset, - final boolean compact, final boolean complete, final boolean eventEol, - final Serializer headerSerializer, - final Serializer footerSerializer, final boolean includeNullDelimiter) { - this(config, objectWriter, charset, compact, complete, eventEol, null, headerSerializer, footerSerializer, - includeNullDelimiter, null); - } - - protected AbstractJacksonLayoutCopy(final Configuration config, final ObjectWriter objectWriter, - final Charset charset, - final boolean compact, final boolean complete, final boolean eventEol, - final String endOfLine, final Serializer headerSerializer, - final Serializer footerSerializer, final boolean includeNullDelimiter, - final KeyValuePair[] additionalFields) { - super(config, charset, headerSerializer, footerSerializer); - this.objectWriter = objectWriter; - this.compact = compact; - this.complete = complete; - this.eol = endOfLine != null ? endOfLine : compact && !eventEol ? COMPACT_EOL : DEFAULT_EOL; - this.includeNullDelimiter = includeNullDelimiter; - this.additionalFields = prepareAdditionalFields(config, additionalFields); - } - - protected static boolean valueNeedsLookup(final String value) { - return value != null && value.contains("${"); - } - - private static ResolvableKeyValuePair[] prepareAdditionalFields(final Configuration config, - final KeyValuePair[] additionalFields) { - if (additionalFields == null || additionalFields.length == 0) { - // No fields set - return ResolvableKeyValuePair.EMPTY_ARRAY; - } - - // Convert to specific class which already determines whether values needs lookup during serialization - final ResolvableKeyValuePair[] resolvableFields = new ResolvableKeyValuePair[additionalFields.length]; - - for (int i = 0; i < additionalFields.length; i++) { - final ResolvableKeyValuePair resolvable = - resolvableFields[i] = new ResolvableKeyValuePair(additionalFields[i]); - - // Validate - if (config == null && resolvable.valueNeedsLookup) { - throw new IllegalArgumentException( - "configuration needs to be set when there are additional fields with variables"); - } - } - - return resolvableFields; - } - - private static LogEvent convertMutableToLog4jEvent(final LogEvent event) { - return event instanceof Log4jLogEvent ? event : Log4jLogEvent.createMemento(event); - } - - /** - * Formats a {@link org.apache.logging.log4j.core.LogEvent}. - * - * @param event The LogEvent. - * @return The XML representation of the LogEvent. - */ - @Override - public String toSerializable(final LogEvent event) { - final StringBuilderWriter writer = new StringBuilderWriter(); - try { - toSerializable(event, writer); - return writer.toString(); - } catch (final IOException e) { - // Should this be an ISE or IAE? - LOGGER.error(e); - return Strings.EMPTY; - } - } - - protected Object wrapLogEvent(final LogEvent event) { - if (additionalFields.length > 0) { - // Construct map for serialization - note that we are intentionally using original LogEvent - final Map additionalFieldsMap = resolveAdditionalFields(event); - // This class combines LogEvent with AdditionalFields during serialization - return new LogEventWithAdditionalFields(event, additionalFieldsMap); - } else if (event instanceof Message) { - // If the LogEvent implements the Messagee interface Jackson will not treat is as a LogEvent. - return new ReadOnlyLogEventWrapper(event); - } else { - // No additional fields, return original object - return event; - } - } - - private Map resolveAdditionalFields(final LogEvent logEvent) { - // Note: LinkedHashMap retains order - final Map additionalFieldsMap = new LinkedHashMap<>(additionalFields.length); - final StrSubstitutor strSubstitutor = configuration.getStrSubstitutor(); - - // Go over each field - for (final ResolvableKeyValuePair pair : additionalFields) { - if (pair.valueNeedsLookup) { - // Resolve value - additionalFieldsMap.put(pair.key, strSubstitutor.replace(logEvent, pair.value)); - } else { - // Plain text value - additionalFieldsMap.put(pair.key, pair.value); - } - } - - return additionalFieldsMap; - } - - public void toSerializable(final LogEvent event, final Writer writer) - throws JsonGenerationException, JsonMappingException, IOException { - objectWriter.writeValue(writer, wrapLogEvent(convertMutableToLog4jEvent(event))); - writer.write(eol); - if (includeNullDelimiter) { - writer.write('\0'); - } - markEvent(); - } - - public static abstract class Builder> extends AbstractStringLayout.Builder { - - @PluginBuilderAttribute - private boolean eventEol; - - @PluginBuilderAttribute - private String endOfLine; - - @PluginBuilderAttribute - private boolean compact; - - @PluginBuilderAttribute - private boolean complete; - - @PluginBuilderAttribute - private boolean locationInfo; - - @PluginBuilderAttribute - private boolean properties; - - @PluginBuilderAttribute - private boolean includeStacktrace = true; - - @PluginBuilderAttribute - private boolean stacktraceAsString = false; - - @PluginBuilderAttribute - private boolean includeNullDelimiter = false; - - @PluginBuilderAttribute - private boolean includeTimeMillis = false; - - @PluginElement("AdditionalField") - private KeyValuePair[] additionalFields; - - protected String toStringOrNull(final byte[] header) { - return header == null ? null : new String(header, Charset.defaultCharset()); - } - - public boolean getEventEol() { - return eventEol; - } - - public B setEventEol(final boolean eventEol) { - this.eventEol = eventEol; - return asBuilder(); - } - - public String getEndOfLine() { - return endOfLine; - } - - public B setEndOfLine(final String endOfLine) { - this.endOfLine = endOfLine; - return asBuilder(); - } - - public boolean isCompact() { - return compact; - } - - public B setCompact(final boolean compact) { - this.compact = compact; - return asBuilder(); - } - - public boolean isComplete() { - return complete; - } - - public B setComplete(final boolean complete) { - this.complete = complete; - return asBuilder(); - } - - public boolean isLocationInfo() { - return locationInfo; - } - - public B setLocationInfo(final boolean locationInfo) { - this.locationInfo = locationInfo; - return asBuilder(); - } - - public boolean isProperties() { - return properties; - } - - public B setProperties(final boolean properties) { - this.properties = properties; - return asBuilder(); - } - - /** - * If "true", includes the stacktrace of any Throwable in the generated data, defaults to "true". - * - * @return If "true", includes the stacktrace of any Throwable in the generated data, defaults to "true". - */ - public boolean isIncludeStacktrace() { - return includeStacktrace; - } - - /** - * If "true", includes the stacktrace of any Throwable in the generated JSON, defaults to "true". - * - * @param includeStacktrace If "true", includes the stacktrace of any Throwable in the generated JSON, defaults to "true". - * @return this builder - */ - public B setIncludeStacktrace(final boolean includeStacktrace) { - this.includeStacktrace = includeStacktrace; - return asBuilder(); - } - - public boolean isStacktraceAsString() { - return stacktraceAsString; - } - - /** - * Whether to format the stacktrace as a string, and not a nested object (optional, defaults to false). - * - * @return this builder - */ - public B setStacktraceAsString(final boolean stacktraceAsString) { - this.stacktraceAsString = stacktraceAsString; - return asBuilder(); - } - - public boolean isIncludeNullDelimiter() { - return includeNullDelimiter; - } - - /** - * Whether to include NULL byte as delimiter after each event (optional, default to false). - * - * @return this builder - */ - public B setIncludeNullDelimiter(final boolean includeNullDelimiter) { - this.includeNullDelimiter = includeNullDelimiter; - return asBuilder(); - } - - public boolean isIncludeTimeMillis() { - return includeTimeMillis; - } - - /** - * Whether to include the timestamp (in addition to the Instant) (optional, default to false). - * - * @return this builder - */ - public B setIncludeTimeMillis(final boolean includeTimeMillis) { - this.includeTimeMillis = includeTimeMillis; - return asBuilder(); - } - - public KeyValuePair[] getAdditionalFields() { - return additionalFields; - } - - /** - * Additional fields to set on each log event. - * - * @return this builder - */ - public B setAdditionalFields(final KeyValuePair[] additionalFields) { - this.additionalFields = additionalFields; - return asBuilder(); - } - } - - @JsonRootName(XmlConstants.ELT_EVENT) - public static class LogEventWithAdditionalFields { - - private final Object logEvent; - private final Map additionalFields; - - public LogEventWithAdditionalFields(final Object logEvent, final Map additionalFields) { - this.logEvent = logEvent; - this.additionalFields = additionalFields; - } - - @JsonUnwrapped - public Object getLogEvent() { - return logEvent; - } - - @JsonAnyGetter - @SuppressWarnings("unused") - public Map getAdditionalFields() { - return additionalFields; - } - } - - protected static class ResolvableKeyValuePair { - - /** - * The empty array. - */ - static final ResolvableKeyValuePair[] EMPTY_ARRAY = {}; - - final String key; - final String value; - final boolean valueNeedsLookup; - - ResolvableKeyValuePair(final KeyValuePair pair) { - this.key = pair.getKey(); - this.value = pair.getValue(); - this.valueNeedsLookup = AbstractJacksonLayoutCopy.valueNeedsLookup(this.value); - } - } - - private static class ReadOnlyLogEventWrapper implements LogEvent { - - @JsonIgnore - private final LogEvent event; - - public ReadOnlyLogEventWrapper(LogEvent event) { - this.event = event; - } - - @Override - public LogEvent toImmutable() { - return event.toImmutable(); - } - - @Override - public Map getContextMap() { - return event.getContextMap(); - } - - @Override - public ReadOnlyStringMap getContextData() { - return event.getContextData(); - } - - @Override - public ThreadContext.ContextStack getContextStack() { - return event.getContextStack(); - } - - @Override - public String getLoggerFqcn() { - return event.getLoggerFqcn(); - } - - @Override - public Level getLevel() { - return event.getLevel(); - } - - @Override - public String getLoggerName() { - return event.getLoggerName(); - } - - @Override - public Marker getMarker() { - return event.getMarker(); - } - - @Override - public Message getMessage() { - return event.getMessage(); - } - - @Override - public long getTimeMillis() { - return event.getTimeMillis(); - } - - @Override - public Instant getInstant() { - return event.getInstant(); - } - - @Override - public StackTraceElement getSource() { - return event.getSource(); - } - - @Override - public String getThreadName() { - return event.getThreadName(); - } - - @Override - public long getThreadId() { - return event.getThreadId(); - } - - @Override - public int getThreadPriority() { - return event.getThreadPriority(); - } - - @Override - public Throwable getThrown() { - return event.getThrown(); - } - - @Override - public ThrowableProxy getThrownProxy() { - return event.getThrownProxy(); - } - - @Override - public boolean isEndOfBatch() { - return event.isEndOfBatch(); - } - - @Override - public void setEndOfBatch(boolean endOfBatch) { - - } - - @Override - public boolean isIncludeLocation() { - return event.isIncludeLocation(); - } - - @Override - public void setIncludeLocation(boolean locationRequired) { - - } - - @Override - public long getNanoTime() { - return event.getNanoTime(); - } - } -} \ No newline at end of file diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/DefautlLoggingManager.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/DefautlLoggingManager.java new file mode 100644 index 000000000..5326f53e6 --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/DefautlLoggingManager.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; + +import org.slf4j.Logger; +import org.slf4j.event.Level; + +/** + * When no LoggingManager is found, setting a default one with no action on logging implementation + * Powertools cannot change the log level based on the environment variable, will use the logger configuration + */ +public class DefautlLoggingManager implements LoggingManager { + + @Override + public void setLogLevel(Level logLevel) { + // do nothing + } + + @Override + public Level getLogLevel(Logger logger) { + return Level.ERROR; + } +} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/JacksonFactoryCopy.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/JacksonFactoryCopy.java deleted file mode 100644 index 6b568be30..000000000 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/JacksonFactoryCopy.java +++ /dev/null @@ -1,133 +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.internal; - -import com.fasterxml.jackson.core.PrettyPrinter; -import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; -import com.fasterxml.jackson.core.util.MinimalPrettyPrinter; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectWriter; -import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter; -import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; -import java.util.HashSet; -import java.util.Set; -import org.apache.logging.log4j.core.impl.Log4jLogEvent; -import org.apache.logging.log4j.core.jackson.JsonConstants; -import org.apache.logging.log4j.core.jackson.Log4jJsonObjectMapper; - -@Deprecated -abstract class JacksonFactoryCopy { - - abstract protected String getPropertyNameForTimeMillis(); - - abstract protected String getPropertyNameForInstant(); - - abstract protected String getPropertNameForContextMap(); - - abstract protected String getPropertNameForSource(); - - abstract protected String getPropertNameForNanoTime(); - - abstract protected PrettyPrinter newCompactPrinter(); - - abstract protected ObjectMapper newObjectMapper(); - - abstract protected PrettyPrinter newPrettyPrinter(); - - ObjectWriter newWriter(final boolean locationInfo, final boolean properties, final boolean compact) { - return newWriter(locationInfo, properties, compact, false); - } - - ObjectWriter newWriter(final boolean locationInfo, final boolean properties, final boolean compact, - final boolean includeMillis) { - final SimpleFilterProvider filters = new SimpleFilterProvider(); - final Set except = new HashSet<>(3); - if (!locationInfo) { - except.add(this.getPropertNameForSource()); - } - if (!properties) { - except.add(this.getPropertNameForContextMap()); - } - if (includeMillis) { - except.add(getPropertyNameForInstant()); - } else { - except.add(getPropertyNameForTimeMillis()); - } - except.add(this.getPropertNameForNanoTime()); - filters.addFilter(Log4jLogEvent.class.getName(), SimpleBeanPropertyFilter.serializeAllExcept(except)); - final ObjectWriter writer = - this.newObjectMapper().writer(compact ? this.newCompactPrinter() : this.newPrettyPrinter()); - return writer.with(filters); - } - - static class JSON extends JacksonFactoryCopy { - - private final boolean encodeThreadContextAsList; - private final boolean includeStacktrace; - private final boolean stacktraceAsString; - private final boolean objectMessageAsJsonObject; - - public JSON(final boolean encodeThreadContextAsList, final boolean includeStacktrace, - final boolean stacktraceAsString, final boolean objectMessageAsJsonObject) { - this.encodeThreadContextAsList = encodeThreadContextAsList; - this.includeStacktrace = includeStacktrace; - this.stacktraceAsString = stacktraceAsString; - this.objectMessageAsJsonObject = objectMessageAsJsonObject; - } - - @Override - protected String getPropertNameForContextMap() { - return JsonConstants.ELT_CONTEXT_MAP; - } - - @Override - protected String getPropertyNameForTimeMillis() { - return JsonConstants.ELT_TIME_MILLIS; - } - - @Override - protected String getPropertyNameForInstant() { - return JsonConstants.ELT_INSTANT; - } - - @Override - protected String getPropertNameForSource() { - return JsonConstants.ELT_SOURCE; - } - - @Override - protected String getPropertNameForNanoTime() { - return JsonConstants.ELT_NANO_TIME; - } - - @Override - protected PrettyPrinter newCompactPrinter() { - return new MinimalPrettyPrinter(); - } - - @Override - protected ObjectMapper newObjectMapper() { - return new Log4jJsonObjectMapper(encodeThreadContextAsList, includeStacktrace, stacktraceAsString, - objectMessageAsJsonObject); - } - - @Override - protected PrettyPrinter newPrettyPrinter() { - return new DefaultPrettyPrinter(); - } - - } - -} \ No newline at end of file diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonLayout.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonLayout.java deleted file mode 100644 index fd646ab50..000000000 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonLayout.java +++ /dev/null @@ -1,246 +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.internal; - -import static java.time.Instant.ofEpochMilli; -import static java.time.format.DateTimeFormatter.ISO_ZONED_DATE_TIME; - -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonGetter; -import com.fasterxml.jackson.annotation.JsonRootName; -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import java.io.IOException; -import java.io.Writer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; -import org.apache.logging.log4j.core.Layout; -import org.apache.logging.log4j.core.LogEvent; -import org.apache.logging.log4j.core.config.Configuration; -import org.apache.logging.log4j.core.config.DefaultConfiguration; -import org.apache.logging.log4j.core.config.Node; -import org.apache.logging.log4j.core.config.plugins.Plugin; -import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; -import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; -import org.apache.logging.log4j.core.jackson.XmlConstants; -import org.apache.logging.log4j.core.layout.PatternLayout; -import org.apache.logging.log4j.core.util.KeyValuePair; -import org.apache.logging.log4j.util.Strings; - -/*** - * Note: The LambdaJsonLayout should be considered to be deprecated. Please use JsonTemplateLayout instead. - */ -@Deprecated -@Plugin(name = "LambdaJsonLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true) -public final class LambdaJsonLayout extends AbstractJacksonLayoutCopy { - static final String CONTENT_TYPE = "application/json"; - private static final String DEFAULT_FOOTER = "]"; - private static final String DEFAULT_HEADER = "["; - - private LambdaJsonLayout(final Configuration config, final boolean locationInfo, final boolean properties, - final boolean encodeThreadContextAsList, - final boolean complete, final boolean compact, final boolean eventEol, - final String headerPattern, final String footerPattern, final Charset charset, - final boolean includeStacktrace, final boolean stacktraceAsString, - final boolean includeNullDelimiter, - final KeyValuePair[] additionalFields, final boolean objectMessageAsJsonObject) { - super(config, new JacksonFactoryCopy.JSON(encodeThreadContextAsList, includeStacktrace, stacktraceAsString, - objectMessageAsJsonObject).newWriter( - locationInfo, properties, compact), - charset, compact, complete, eventEol, - null, - PatternLayout.newSerializerBuilder().setConfiguration(config).setPattern(headerPattern) - .setDefaultPattern(DEFAULT_HEADER).build(), - PatternLayout.newSerializerBuilder().setConfiguration(config).setPattern(footerPattern) - .setDefaultPattern(DEFAULT_FOOTER).build(), - includeNullDelimiter, - additionalFields); - } - - @PluginBuilderFactory - public static > B newBuilder() { - return new Builder().asBuilder(); - } - - /** - * Creates a JSON Layout using the default settings. Useful for testing. - * - * @return A JSON Layout. - */ - public static LambdaJsonLayout createDefaultLayout() { - return new LambdaJsonLayout(new DefaultConfiguration(), false, false, false, false, false, false, - DEFAULT_HEADER, DEFAULT_FOOTER, StandardCharsets.UTF_8, true, false, false, null, false); - } - - /** - * Returns appropriate JSON header. - * - * @return a byte array containing the header, opening the JSON array. - */ - @Override - public byte[] getHeader() { - if (!this.complete) { - return null; - } - final StringBuilder buf = new StringBuilder(); - final String str = serializeToString(getHeaderSerializer()); - if (str != null) { - buf.append(str); - } - buf.append(this.eol); - return getBytes(buf.toString()); - } - - /** - * Returns appropriate JSON footer. - * - * @return a byte array containing the footer, closing the JSON array. - */ - @Override - public byte[] getFooter() { - if (!this.complete) { - return null; - } - final StringBuilder buf = new StringBuilder(); - buf.append(this.eol); - final String str = serializeToString(getFooterSerializer()); - if (str != null) { - buf.append(str); - } - buf.append(this.eol); - return getBytes(buf.toString()); - } - - @Override - public Map getContentFormat() { - final Map result = new HashMap<>(); - result.put("version", "2.0"); - return result; - } - - /** - * @return The content type. - */ - @Override - public String getContentType() { - return CONTENT_TYPE + "; charset=" + this.getCharset(); - } - - @Override - public Object wrapLogEvent(final LogEvent event) { - Map additionalFieldsMap = resolveAdditionalFields(event); - // This class combines LogEvent with AdditionalFields during serialization - return new LogEventWithAdditionalFields(event, additionalFieldsMap); - } - - @Override - public void toSerializable(final LogEvent event, final Writer writer) throws IOException { - if (complete && eventCount > 0) { - writer.append(", "); - } - super.toSerializable(event, writer); - } - - private Map resolveAdditionalFields(LogEvent logEvent) { - // Note: LinkedHashMap retains order - final Map additionalFieldsMap = new LinkedHashMap<>(additionalFields.length); - - // Go over MDC - logEvent.getContextData().forEach((key, value) -> - { - if (Strings.isNotBlank(key) && value != null) { - additionalFieldsMap.put(key, value); - } - }); - - return additionalFieldsMap; - } - - public static class Builder> extends AbstractJacksonLayoutCopy.Builder - implements org.apache.logging.log4j.core.util.Builder { - - @PluginBuilderAttribute - private boolean propertiesAsList; - - @PluginBuilderAttribute - private boolean objectMessageAsJsonObject; - - public Builder() { - super(); - setCharset(StandardCharsets.UTF_8); - } - - @Override - public LambdaJsonLayout build() { - final boolean encodeThreadContextAsList = isProperties() && propertiesAsList; - final String headerPattern = toStringOrNull(getHeader()); - final String footerPattern = toStringOrNull(getFooter()); - return new LambdaJsonLayout(getConfiguration(), isLocationInfo(), isProperties(), encodeThreadContextAsList, - isComplete(), isCompact(), getEventEol(), headerPattern, footerPattern, getCharset(), - isIncludeStacktrace(), isStacktraceAsString(), isIncludeNullDelimiter(), - getAdditionalFields(), getObjectMessageAsJsonObject()); - } - - public boolean isPropertiesAsList() { - return propertiesAsList; - } - - public B setPropertiesAsList(final boolean propertiesAsList) { - this.propertiesAsList = propertiesAsList; - return asBuilder(); - } - - public boolean getObjectMessageAsJsonObject() { - return objectMessageAsJsonObject; - } - - public B setObjectMessageAsJsonObject(final boolean objectMessageAsJsonObject) { - this.objectMessageAsJsonObject = objectMessageAsJsonObject; - return asBuilder(); - } - } - - @JsonRootName(XmlConstants.ELT_EVENT) - public static class LogEventWithAdditionalFields { - - private final LogEvent logEvent; - private final Map additionalFields; - - public LogEventWithAdditionalFields(LogEvent logEvent, Map additionalFields) { - this.logEvent = logEvent; - this.additionalFields = additionalFields; - } - - @JsonUnwrapped - public Object getLogEvent() { - return logEvent; - } - - @JsonAnyGetter - public Map getAdditionalFields() { - return additionalFields; - } - - @JsonGetter("timestamp") - public String getTimestamp() { - return ISO_ZONED_DATE_TIME.format( - ZonedDateTime.from(ofEpochMilli(logEvent.getTimeMillis()).atZone(ZoneId.systemDefault()))); - } - } -} 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 2e17ce692..48f67700d 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 @@ -27,80 +27,146 @@ 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.LoggingUtils.objectMapper; 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.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.JsonPointer; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; - +import io.burt.jmespath.Expression; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStream; import java.io.OutputStreamWriter; -import java.util.Map; +import java.io.PrintStream; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Optional; import java.util.Random; - -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.ThreadContext; -import org.apache.logging.log4j.core.LoggerContext; -import org.apache.logging.log4j.core.config.Configurator; -import org.apache.logging.log4j.core.util.IOUtils; +import java.util.ServiceLoader; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.DeclarePrecedence; import org.aspectj.lang.annotation.Pointcut; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +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; + @Aspect @DeclarePrecedence("*, software.amazon.lambda.powertools.logging.internal.LambdaLoggingAspect") public final class LambdaLoggingAspect { - private static final Logger LOG = LogManager.getLogger(LambdaLoggingAspect.class); - private static final String POWERTOOLS_LOG_LEVEL = System.getenv("POWERTOOLS_LOG_LEVEL"); - + private static final Logger LOG = LoggerFactory.getLogger(LambdaLoggingAspect.class); private static final Random SAMPLER = new Random(); - private static final String SAMPLING_RATE = System.getenv("POWERTOOLS_LOGGER_SAMPLE_RATE"); - private static Boolean LOG_EVENT; + private static Level LEVEL_AT_INITIALISATION; /* not final for test purpose */ - private static Level LEVEL_AT_INITIALISATION; + private static final LoggingManager LOGGING_MANAGER; static { + LOGGING_MANAGER = getLoggingManagerFromServiceLoader(); + + setLogLevel(); + + LEVEL_AT_INITIALISATION = LOGGING_MANAGER.getLogLevel(LOG); + } + + static void setLogLevel() { if (POWERTOOLS_LOG_LEVEL != null) { - Level powertoolsLevel = Level.getLevel(POWERTOOLS_LOG_LEVEL); + Level powertoolsLevel = getLevelFromString(POWERTOOLS_LOG_LEVEL); if (LAMBDA_LOG_LEVEL != null) { - Level lambdaLevel = Level.getLevel(LAMBDA_LOG_LEVEL); - if (powertoolsLevel.intLevel() > lambdaLevel.intLevel()) { + Level lambdaLevel = getLevelFromString(LAMBDA_LOG_LEVEL); + if (powertoolsLevel.toInt() < lambdaLevel.toInt()) { LOG.warn("Current log level ({}) does not match AWS Lambda Advanced Logging Controls minimum log level ({}). This can lead to data loss, consider adjusting them.", POWERTOOLS_LOG_LEVEL, LAMBDA_LOG_LEVEL); } } - resetLogLevels(powertoolsLevel); + setLogLevels(powertoolsLevel); } else if (LAMBDA_LOG_LEVEL != null) { - resetLogLevels(Level.getLevel(LAMBDA_LOG_LEVEL)); + setLogLevels(getLevelFromString(LAMBDA_LOG_LEVEL)); } + } - LEVEL_AT_INITIALISATION = LOG.getLevel(); + private static Level getLevelFromString(String level) { + if (Arrays.stream(Level.values()).anyMatch(slf4jLevel -> slf4jLevel.name().equalsIgnoreCase(level))) { + return Level.valueOf(level.toUpperCase()); + } else { + // FATAL does not exist in slf4j + if ("FATAL".equalsIgnoreCase(level)) { + return Level.ERROR; + } + } + // default to INFO if incorrect value + return Level.INFO; + } + + /** + * Use {@link ServiceLoader} to lookup for a {@link LoggingManager}. + * A file software.amazon.lambda.powertools.logging.internal.LoggingManager must be created in + * META-INF/services/ folder with the appropriate implementation of the {@link LoggingManager} + * + * @return an instance of {@link LoggingManager} + * @throws IllegalStateException if no {@link LoggingManager} could be found + */ + @SuppressWarnings("java:S106") // S106: System.err is used rather than logger to make sure message is printed + private static LoggingManager getLoggingManagerFromServiceLoader() { + ServiceLoader loggingManagers; + SecurityManager securityManager = System.getSecurityManager(); + if (securityManager == null) { + loggingManagers = ServiceLoader.load(LoggingManager.class); + } else { + final PrivilegedAction> action = () -> ServiceLoader.load(LoggingManager.class); + loggingManagers = AccessController.doPrivileged(action); + } + + List loggingManagerList = new ArrayList<>(); + for (LoggingManager lm : loggingManagers) { + loggingManagerList.add(lm); + } + return getLoggingManager(loggingManagerList, System.err); + } - String logEvent = System.getenv("POWERTOOLS_LOGGER_LOG_EVENT"); - if (logEvent != null) { - LOG_EVENT = Boolean.parseBoolean(logEvent); + static LoggingManager getLoggingManager(List loggingManagerList, PrintStream printStream) { + LoggingManager loggingManager; + if (loggingManagerList.isEmpty()) { + printStream.println("ERROR. No LoggingManager was found on the classpath"); + printStream.println("ERROR. Applying default LoggingManager: POWERTOOLS_LOG_LEVEL variable is ignored"); + printStream.println("ERROR. Make sure to add either powertools-logging-log4j or powertools-logging-logback to your dependencies"); + loggingManager = new DefautlLoggingManager(); } else { - LOG_EVENT = false; + if (loggingManagerList.size() > 1) { + printStream.println("WARN. Multiple LoggingManagers were found on the classpath"); + for (LoggingManager manager : loggingManagerList) { + printStream.println("WARN. Found LoggingManager: [" + manager + "]"); + } + printStream.println("WARN. Make sure to have only one of powertools-logging-log4j OR powertools-logging-logback to your dependencies"); + printStream.println("WARN. Using the first LoggingManager found on the classpath: [" + loggingManagerList.get(0) + "]"); + } + loggingManager = loggingManagerList.get(0); } + return loggingManager; } - private static void resetLogLevels(Level logLevel) { - LoggerContext ctx = (LoggerContext) LogManager.getContext(false); - Configurator.setAllLevels(LogManager.getRootLogger().getName(), logLevel); - ctx.updateLoggers(); + private static void setLogLevels(Level logLevel) { + LOGGING_MANAGER.setLogLevel(logLevel); } @SuppressWarnings({"EmptyMethod"}) @@ -108,41 +174,91 @@ private static void resetLogLevels(Level logLevel) { public void callAt(Logging logging) { } + /** + * Main method of the aspect + */ @Around(value = "callAt(logging) && execution(@Logging * *.*(..))", argNames = "pjp,logging") public Object around(ProceedingJoinPoint pjp, Logging logging) throws Throwable { - Object[] proceedArgs = pjp.getArgs(); + + boolean isOnRequestHandler = placedOnRequestHandler(pjp); + boolean isOnRequestStreamHandler = placedOnStreamHandler(pjp); setLogLevelBasedOnSamplingRate(pjp, logging); - Context extractedContext = extractContext(pjp); + addLambdaContextToLoggingContext(pjp); + + getXrayTraceId().ifPresent(xRayTraceId -> appendKey(FUNCTION_TRACE_ID.getName(), xRayTraceId)); + + Object[] proceedArgs = logEvent(pjp, logging, isOnRequestHandler, isOnRequestStreamHandler); + + if (!logging.correlationIdPath().isEmpty()) { + captureCorrelationId(logging.correlationIdPath(), proceedArgs, isOnRequestHandler, isOnRequestStreamHandler); + } - if (null != extractedContext) { - appendKeys(DefaultLambdaFields.values(extractedContext)); - appendKey("coldStart", isColdStart() ? "true" : "false"); - appendKey("service", serviceName()); + // To log the result of a RequestStreamHandler (OutputStream), we need to do the following: + // 1. backup a reference to the OutputStream provided by Lambda + // 2. create a temporary OutputStream and pass it to the handler method + // 3. retrieve this temporary stream to log it (if enabled) + // 4. write it back to the OutputStream provided by Lambda + OutputStream backupOutputStream = null; + if ((logging.logResponse() || POWERTOOLS_LOG_RESPONSE) && isOnRequestStreamHandler) { + backupOutputStream = (OutputStream) proceedArgs[1]; + proceedArgs[1] = new ByteArrayOutputStream(); } - getXrayTraceId().ifPresent(xRayTraceId -> appendKey("xray_trace_id", xRayTraceId)); + Object lambdaFunctionResponse; - // Check that the environment variable was enabled explicitly - // Or that the handler was annotated with @Logging(logEvent = true) - if (LOG_EVENT || logging.logEvent()) { - proceedArgs = logEvent(pjp); + try { + lambdaFunctionResponse = pjp.proceed(proceedArgs); + } catch (Throwable t) { + if (logging.logError() || POWERTOOLS_LOG_ERROR) { + // logging the exception with additional context + logger(pjp).error(MarkerFactory.getMarker("FATAL"), "Exception in Lambda Handler", t); + } + throw t; + } finally { + if (logging.clearState()) { + MDC.clear(); + } + coldStartDone(); } - if (!logging.correlationIdPath().isEmpty()) { - proceedArgs = captureCorrelationId(logging.correlationIdPath(), pjp); + if ((logging.logResponse() || POWERTOOLS_LOG_RESPONSE)) { + if (isOnRequestHandler) { + logRequestHandlerResponse(pjp, lambdaFunctionResponse); + } else if (isOnRequestStreamHandler && backupOutputStream != null) { + byte[] bytes = ((ByteArrayOutputStream)proceedArgs[1]).toByteArray(); + logRequestStreamHandlerResponse(pjp, bytes); + backupOutputStream.write(bytes); + } } - Object proceed = pjp.proceed(proceedArgs); + return lambdaFunctionResponse; + } + + private Object[] logEvent(ProceedingJoinPoint pjp, Logging logging, + boolean isOnRequestHandler, boolean isOnRequestStreamHandler) { + Object[] proceedArgs = pjp.getArgs(); - if (logging.clearState()) { - ThreadContext.clearMap(); + if (logging.logEvent() || POWERTOOLS_LOG_EVENT) { + if (isOnRequestHandler) { + logRequestHandlerEvent(pjp, pjp.getArgs()[0]); + } else if (isOnRequestStreamHandler) { + proceedArgs = logRequestStreamHandlerEvent(pjp); + } } + return proceedArgs; + } + + private void addLambdaContextToLoggingContext(ProceedingJoinPoint pjp) { + Context extractedContext = extractContext(pjp); - coldStartDone(); - return proceed; + if (extractedContext != null) { + appendKeys(PowertoolsLoggedFields.setValuesFromLambdaContext(extractedContext)); + appendKey(FUNCTION_COLD_START.getName(), isColdStart() ? "true" : "false"); + appendKey(SERVICE.getName(), serviceName()); + } } private void setLogLevelBasedOnSamplingRate(final ProceedingJoinPoint pjp, @@ -152,12 +268,12 @@ private void setLogLevelBasedOnSamplingRate(final ProceedingJoinPoint pjp, if (isHandlerMethod(pjp)) { if (samplingRate < 0 || samplingRate > 1) { - LOG.debug("Skipping sampling rate configuration because of invalid value. Sampling rate: {}", + LOG.warn("Skipping sampling rate configuration because of invalid value. Sampling rate: {}", samplingRate); return; } - appendKey("samplingRate", String.valueOf(samplingRate)); + appendKey(PowertoolsLoggedFields.SAMPLING_RATE.getName(), String.valueOf(samplingRate)); if (samplingRate == 0) { return; @@ -166,130 +282,132 @@ private void setLogLevelBasedOnSamplingRate(final ProceedingJoinPoint pjp, float sample = SAMPLER.nextFloat(); if (samplingRate > sample) { - resetLogLevels(Level.DEBUG); + setLogLevels(Level.DEBUG); - LOG.debug("Changed log level to DEBUG based on Sampling configuration. " + - "Sampling Rate: {}, Sampler Value: {}.", samplingRate, sample); - } else if (LEVEL_AT_INITIALISATION != LOG.getLevel()) { - resetLogLevels(LEVEL_AT_INITIALISATION); + LOG.debug("Changed log level to DEBUG based on Sampling configuration. " + + "Sampling Rate: {}, Sampler Value: {}.", samplingRate, sample); + } else if (LEVEL_AT_INITIALISATION != LOGGING_MANAGER.getLogLevel(LOG)) { + setLogLevels(LEVEL_AT_INITIALISATION); } } } private double samplingRate(final Logging logging) { - if (null != SAMPLING_RATE) { + String sampleRate = POWERTOOLS_SAMPLING_RATE; + if (null != sampleRate) { try { - return Double.parseDouble(SAMPLING_RATE); + return Double.parseDouble(sampleRate); } catch (NumberFormatException e) { - LOG.debug("Skipping sampling rate on environment variable configuration because of invalid " + - "value. Sampling rate: {}", SAMPLING_RATE); + LOG.warn("Skipping sampling rate on environment variable configuration because of invalid " + + "value. Sampling rate: {}", sampleRate); } } return logging.samplingRate(); } - private Object[] logEvent(final ProceedingJoinPoint pjp) { - Object[] args = pjp.getArgs(); - - if (isHandlerMethod(pjp)) { - if (placedOnRequestHandler(pjp)) { - Logger log = logger(pjp); - asJson(pjp, pjp.getArgs()[0]) - .ifPresent(log::info); - } + 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); + } + } - if (placedOnStreamHandler(pjp)) { - args = logFromInputStream(pjp); + 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)); + } catch (IOException e) { + LOG.warn("Failed to log event from supplied input stream.", e); } + LoggingUtils.logMessagesAsJson(false); } - return args; } - private Object[] captureCorrelationId(final String correlationIdPath, - final ProceedingJoinPoint pjp) { - Object[] args = pjp.getArgs(); - if (isHandlerMethod(pjp)) { - if (placedOnRequestHandler(pjp)) { - Object arg = pjp.getArgs()[0]; - JsonNode jsonNode = objectMapper().valueToTree(arg); - - setCorrelationIdFromNode(correlationIdPath, pjp, jsonNode); - - return args; - } + 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); + } + } - if (placedOnStreamHandler(pjp)) { - try { - byte[] bytes = bytesFromInputStreamSafely((InputStream) pjp.getArgs()[0]); - JsonNode jsonNode = objectMapper().readTree(bytes); - args[0] = new ByteArrayInputStream(bytes); + 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); + } + } - setCorrelationIdFromNode(correlationIdPath, pjp, jsonNode); + private void captureCorrelationId(final String correlationIdPath, + Object[] proceedArgs, + final boolean isOnRequestHandler, + final boolean isOnRequestStreamHandler) { + if (isOnRequestHandler) { + JsonNode jsonNode = LoggingUtils.getObjectMapper().valueToTree(proceedArgs[0]); + setCorrelationIdFromNode(correlationIdPath, jsonNode); + } else if (isOnRequestStreamHandler) { + try { + byte[] bytes = bytesFromInputStreamSafely((InputStream) proceedArgs[0]); + JsonNode jsonNode = LoggingUtils.getObjectMapper().readTree(bytes); + proceedArgs[0] = new ByteArrayInputStream(bytes); - return args; - } catch (IOException e) { - Logger log = logger(pjp); - log.warn("Failed to capture correlation id on event from supplied input stream.", e); - } + setCorrelationIdFromNode(correlationIdPath, jsonNode); + } catch (IOException e) { + LOG.warn("Failed to capture correlation id on event from supplied input stream.", e); } } - - return args; } - private void setCorrelationIdFromNode(String correlationIdPath, ProceedingJoinPoint pjp, JsonNode jsonNode) { - JsonNode node = jsonNode.at(JsonPointer.compile(correlationIdPath)); + private void setCorrelationIdFromNode(String correlationIdPath, JsonNode jsonNode) { + Expression jmesExpression = JsonConfig.get().getJmesPath().compile(correlationIdPath); + JsonNode node = jmesExpression.search(jsonNode); String asText = node.asText(); if (null != asText && !asText.isEmpty()) { LoggingUtils.setCorrelationId(asText); } else { - logger(pjp).debug("Unable to extract any correlation id. Is your function expecting supported event type?"); + LOG.warn("Unable to extract any correlation id. Is your function expecting supported event type?"); } } - private Object[] logFromInputStream(final ProceedingJoinPoint pjp) { - Object[] args = pjp.getArgs(); - - try { - byte[] bytes = bytesFromInputStreamSafely((InputStream) pjp.getArgs()[0]); - args[0] = new ByteArrayInputStream(bytes); - Logger log = logger(pjp); - - asJson(pjp, objectMapper().readValue(bytes, Map.class)) - .ifPresent(log::info); - - } catch (IOException e) { - Logger log = logger(pjp); - log.debug("Failed to log event from supplied input stream.", e); - } - - return args; - } private byte[] bytesFromInputStreamSafely(final InputStream inputStream) throws IOException { try (ByteArrayOutputStream out = new ByteArrayOutputStream(); - InputStreamReader reader = new InputStreamReader(inputStream, UTF_8)) { + InputStreamReader reader = new InputStreamReader(inputStream, UTF_8)) { OutputStreamWriter writer = new OutputStreamWriter(out, UTF_8); - - IOUtils.copy(reader, writer); + int n; + char[] buffer = new char[4096]; + while (-1 != (n = reader.read(buffer))) { + writer.write(buffer, 0, n); + } writer.flush(); return out.toByteArray(); } } - private Optional asJson(final ProceedingJoinPoint pjp, - final Object target) { + private Optional asJson(final Object target) { try { - return ofNullable(objectMapper().writeValueAsString(target)); + return ofNullable(LoggingUtils.getObjectMapper().writeValueAsString(target)); } catch (JsonProcessingException e) { - logger(pjp).error("Failed logging event of type {}", target.getClass(), e); + LOG.error("Failed logging object of type {}", target.getClass(), e); return empty(); } } private Logger logger(final ProceedingJoinPoint pjp) { - return LogManager.getLogger(pjp.getSignature().getDeclaringType()); + return LoggerFactory.getLogger(pjp.getSignature().getDeclaringType()); } } diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaTimestampResolver.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaTimestampResolver.java deleted file mode 100644 index 500b36c95..000000000 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaTimestampResolver.java +++ /dev/null @@ -1,169 +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.internal; - -import static software.amazon.lambda.powertools.logging.internal.LoggingConstants.LAMBDA_LOG_FORMAT; -import static software.amazon.lambda.powertools.logging.internal.LoggingConstants.LOG_DATE_RFC3339_FORMAT; - -import java.util.Locale; -import java.util.TimeZone; -import org.apache.logging.log4j.core.LogEvent; -import org.apache.logging.log4j.core.time.MutableInstant; -import org.apache.logging.log4j.layout.template.json.JsonTemplateLayoutDefaults; -import org.apache.logging.log4j.layout.template.json.resolver.EventResolver; -import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverConfig; -import org.apache.logging.log4j.layout.template.json.util.InstantFormatter; -import org.apache.logging.log4j.layout.template.json.util.JsonWriter; - -/** - * Default timestamp used by log4j is not RFC3339, which is used by Lambda internally to filter logs. - * When `AWS_LAMBDA_LOG_FORMAT` is set to JSON (i.e. using Lambda logging configuration), we should use the appropriate pattern, - * otherwise logs with invalid date format are considered as INFO. - * Inspired from org.apache.logging.log4j.layout.template.json.resolver.TimestampResolver - * - * TODO: remove in v2 an replace with the good pattern in LambdaJsonLayout.json - */ -public class LambdaTimestampResolver implements EventResolver { - - private final EventResolver internalResolver; - - public LambdaTimestampResolver(final TemplateResolverConfig config) { - final PatternResolverContext patternResolverContext = - PatternResolverContext.fromConfig(config); - internalResolver = new PatternResolver(patternResolverContext); - } - - @Override - public void resolve(LogEvent value, JsonWriter jsonWriter) { - internalResolver.resolve(value, jsonWriter); - } - - static String getName() { - return "lambda-timestamp"; - } - - private static final class PatternResolverContext { - - public static final String PATTERN = "pattern"; - private final InstantFormatter formatter; - - private final StringBuilder lastFormattedInstantBuffer = new StringBuilder(); - - private final MutableInstant lastFormattedInstant = new MutableInstant(); - - private PatternResolverContext( - final String pattern, - final TimeZone timeZone, - final Locale locale) { - this.formatter = InstantFormatter - .newBuilder() - .setPattern(pattern) - .setTimeZone(timeZone) - .setLocale(locale) - .build(); - lastFormattedInstant.initFromEpochSecond(-1, 0); - } - - private static PatternResolverContext fromConfig( - final TemplateResolverConfig config) { - final String pattern = readPattern(config); - final TimeZone timeZone = readTimeZone(config); - final Locale locale = config.getLocale(new String[]{PATTERN, "locale"}); - return new PatternResolverContext(pattern, timeZone, locale); - } - - private static String readPattern(final TemplateResolverConfig config) { - final String format = config.getString(new String[]{PATTERN, "format"}); - return format != null - ? format - : getLambdaTimestampFormatOrDefault(); - } - - private static String getLambdaTimestampFormatOrDefault() { - return "JSON".equals(LAMBDA_LOG_FORMAT) ? LOG_DATE_RFC3339_FORMAT : - JsonTemplateLayoutDefaults.getTimestampFormatPattern(); - } - - private static TimeZone readTimeZone(final TemplateResolverConfig config) { - final String timeZoneId = config.getString(new String[]{PATTERN, "timeZone"}); - if (timeZoneId == null) { - return JsonTemplateLayoutDefaults.getTimeZone(); - } - boolean found = false; - for (final String availableTimeZone : TimeZone.getAvailableIDs()) { - if (availableTimeZone.equalsIgnoreCase(timeZoneId)) { - found = true; - break; - } - } - if (!found) { - throw new IllegalArgumentException( - "invalid timestamp time zone: " + config); - } - return TimeZone.getTimeZone(timeZoneId); - } - - } - - private static final class PatternResolver implements EventResolver { - - private final PatternResolverContext patternResolverContext; - - private PatternResolver(final PatternResolverContext patternResolverContext) { - this.patternResolverContext = patternResolverContext; - } - - @Override - public synchronized void resolve( - final LogEvent logEvent, - final JsonWriter jsonWriter) { - - // Format timestamp if it doesn't match the last cached one. - final boolean instantMatching = patternResolverContext.formatter.isInstantMatching( - patternResolverContext.lastFormattedInstant, - logEvent.getInstant()); - if (!instantMatching) { - - // Format the timestamp. - patternResolverContext.lastFormattedInstantBuffer.setLength(0); - patternResolverContext.lastFormattedInstant.initFrom(logEvent.getInstant()); - patternResolverContext.formatter.format( - patternResolverContext.lastFormattedInstant, - patternResolverContext.lastFormattedInstantBuffer); - - // Write the formatted timestamp. - final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder(); - final int startIndex = jsonWriterStringBuilder.length(); - jsonWriter.writeString(patternResolverContext.lastFormattedInstantBuffer); - - // Cache the written value. - patternResolverContext.lastFormattedInstantBuffer.setLength(0); - patternResolverContext.lastFormattedInstantBuffer.append( - jsonWriterStringBuilder, - startIndex, - jsonWriterStringBuilder.length()); - - } - - // Write the cached formatted timestamp. - else { - jsonWriter.writeRawString( - patternResolverContext.lastFormattedInstantBuffer); - } - - } - - } -} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaTimestampResolverFactory.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaTimestampResolverFactory.java deleted file mode 100644 index 2022c6d4a..000000000 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaTimestampResolverFactory.java +++ /dev/null @@ -1,49 +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.internal; - -import org.apache.logging.log4j.core.LogEvent; -import org.apache.logging.log4j.core.config.plugins.Plugin; -import org.apache.logging.log4j.core.config.plugins.PluginFactory; -import org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext; -import org.apache.logging.log4j.layout.template.json.resolver.EventResolverFactory; -import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolver; -import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverConfig; -import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverFactory; - -@Plugin(name = "LambdaTimestampResolverFactory", category = TemplateResolverFactory.CATEGORY) -public final class LambdaTimestampResolverFactory implements EventResolverFactory { - - private static final LambdaTimestampResolverFactory INSTANCE = new LambdaTimestampResolverFactory(); - - private LambdaTimestampResolverFactory() { - } - - @PluginFactory - public static LambdaTimestampResolverFactory getInstance() { - return INSTANCE; - } - - @Override - public String getName() { - return LambdaTimestampResolver.getName(); - } - - @Override - public TemplateResolver create(EventResolverContext context, - TemplateResolverConfig config) { - return new LambdaTimestampResolver(config); - } -} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingConstants.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingConstants.java index e58ca4109..989608a77 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingConstants.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingConstants.java @@ -14,12 +14,18 @@ package software.amazon.lambda.powertools.logging.internal; -public class LoggingConstants { - public static final String LAMBDA_LOG_LEVEL = System.getenv("AWS_LAMBDA_LOG_LEVEL"); +class LoggingConstants { + static String LAMBDA_LOG_LEVEL = System.getenv("AWS_LAMBDA_LOG_LEVEL"); /* not final for test purpose */ - public static final String LAMBDA_LOG_FORMAT = System.getenv("AWS_LAMBDA_LOG_FORMAT"); + static String POWERTOOLS_LOG_LEVEL = System.getenv("POWERTOOLS_LOG_LEVEL"); /* not final for test purpose */ - public static final String LOG_DATE_RFC3339_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; + static String POWERTOOLS_SAMPLING_RATE = System.getenv("POWERTOOLS_LOGGER_SAMPLE_RATE"); /* not final for test purpose */ + + static boolean POWERTOOLS_LOG_EVENT = "true".equals(System.getenv("POWERTOOLS_LOGGER_LOG_EVENT")); /* not final for test purpose */ + + static boolean POWERTOOLS_LOG_RESPONSE = "true".equals(System.getenv("POWERTOOLS_LOGGER_LOG_RESPONSE")); /* not final for test purpose */ + + static boolean POWERTOOLS_LOG_ERROR = "true".equals(System.getenv("POWERTOOLS_LOGGER_LOG_ERROR")); /* not final for test purpose */ private LoggingConstants() { // constants diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManager.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManager.java new file mode 100644 index 000000000..51d05b25d --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LoggingManager.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.internal; + +import org.slf4j.Logger; +import org.slf4j.event.Level; + +/** + * Due to limitations of SLF4J, we need to rely on implementations for some operations: + *

    + *
  • Accessing to all loggers and change their Level
  • + *
  • Retrieving the log Level of a Logger
  • + *
+ * + *

+ * This interface is used for these operations and implementations are provided in submodules and loaded thanks to a {@link java.util.ServiceLoader} + * (define a file named software.amazon.lambda.powertools.logging.internal.LoggingManager + * in src/main/resources/META-INF/services with the qualified name of the implementation). + */ +public interface LoggingManager { + /** + * Change the log Level of all loggers (named and root) + * + * @param logLevel the log Level (slf4j) to apply + */ + void setLogLevel(Level logLevel); + + /** + * Retrieve the log Level of a specific logger + * + * @param logger the logger (slf4j) for which to retrieve the log Level + * @return the Level (slf4j) of this logger. + * Note that SLF4J only support ERROR, WARN, INFO, DEBUG, TRACE while some frameworks may support others (OFF, FATAL, ...) + */ + Level getLogLevel(Logger logger); +} diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/DefaultLambdaFields.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsLoggedFields.java similarity index 59% rename from powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/DefaultLambdaFields.java rename to powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsLoggedFields.java index 2461ae771..6e0047f4f 100644 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/DefaultLambdaFields.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsLoggedFields.java @@ -16,22 +16,38 @@ import com.amazonaws.services.lambda.runtime.Context; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; -enum DefaultLambdaFields { - FUNCTION_NAME("functionName"), - FUNCTION_VERSION("functionVersion"), - FUNCTION_ARN("functionArn"), - FUNCTION_MEMORY_SIZE("functionMemorySize"), - FUNCTION_REQUEST_ID("function_request_id"); +/** + * Fields added in the logs by Powertools. + * Same as python + */ +public enum PowertoolsLoggedFields { + FUNCTION_NAME("function_name"), + FUNCTION_VERSION("function_version"), + FUNCTION_ARN("function_arn"), + FUNCTION_MEMORY_SIZE("function_memory_size"), + FUNCTION_REQUEST_ID("function_request_id"), + FUNCTION_COLD_START("cold_start"), + FUNCTION_TRACE_ID("xray_trace_id"), + SAMPLING_RATE("sampling_rate"), + CORRELATION_ID("correlation_id"), + SERVICE("service"); private final String name; - DefaultLambdaFields(String name) { + PowertoolsLoggedFields(String name) { this.name = name; } - static Map values(Context context) { + public static List stringValues() { + return Stream.of(values()).map(PowertoolsLoggedFields::getName).collect(Collectors.toList()); + } + + static Map setValuesFromLambdaContext(Context context) { Map hashMap = new HashMap<>(); hashMap.put(FUNCTION_NAME.name, context.getFunctionName()); diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsResolver.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsResolver.java deleted file mode 100644 index dc9816932..000000000 --- a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/PowertoolsResolver.java +++ /dev/null @@ -1,66 +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.internal; - -import org.apache.logging.log4j.core.LogEvent; -import org.apache.logging.log4j.layout.template.json.resolver.EventResolver; -import org.apache.logging.log4j.layout.template.json.util.JsonWriter; -import org.apache.logging.log4j.util.ReadOnlyStringMap; - -final class PowertoolsResolver implements EventResolver { - - private final EventResolver internalResolver; - - PowertoolsResolver() { - internalResolver = new EventResolver() { - @Override - public boolean isResolvable(LogEvent value) { - ReadOnlyStringMap contextData = value.getContextData(); - return null != contextData && !contextData.isEmpty(); - } - - @Override - public void resolve(LogEvent logEvent, JsonWriter jsonWriter) { - StringBuilder stringBuilder = jsonWriter.getStringBuilder(); - // remove dummy field to kick inn powertools resolver - stringBuilder.setLength(stringBuilder.length() - 4); - - // Inject all the context information. - ReadOnlyStringMap contextData = logEvent.getContextData(); - contextData.forEach((key, value) -> - { - jsonWriter.writeSeparator(); - jsonWriter.writeString(key); - stringBuilder.append(':'); - jsonWriter.writeValue(value); - }); - } - }; - } - - static String getName() { - return "powertools"; - } - - @Override - public void resolve(LogEvent value, JsonWriter jsonWriter) { - internalResolver.resolve(value, jsonWriter); - } - - @Override - public boolean isResolvable(LogEvent value) { - return internalResolver.isResolvable(value); - } -} diff --git a/powertools-logging/src/main/resources/LambdaEcsLayout.json b/powertools-logging/src/main/resources/LambdaEcsLayout.json deleted file mode 100644 index 4ab9c7ce2..000000000 --- a/powertools-logging/src/main/resources/LambdaEcsLayout.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "@timestamp": { - "$resolver": "timestamp", - "pattern": { - "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", - "timeZone": "UTC" - } - }, - "ecs.version": "1.2.0", - "log.level": { - "$resolver": "level", - "field": "name" - }, - "message": { - "$resolver": "message", - "stringified": true - }, - "process.thread.name": { - "$resolver": "thread", - "field": "name" - }, - "log.logger": { - "$resolver": "logger", - "field": "name" - }, - "labels": { - "$resolver": "mdc", - "flatten": true, - "stringified": true - }, - "tags": { - "$resolver": "ndc" - }, - "error.type": { - "$resolver": "exception", - "field": "className" - }, - "error.message": { - "$resolver": "exception", - "field": "message" - }, - "error.stack_trace": { - "$resolver": "exception", - "field": "stackTrace", - "stackTrace": { - "stringified": true - } - }, - "": { - "$resolver": "powertools" - } -} \ No newline at end of file diff --git a/powertools-logging/src/main/resources/LambdaJsonLayout.json b/powertools-logging/src/main/resources/LambdaJsonLayout.json deleted file mode 100644 index da3385032..000000000 --- a/powertools-logging/src/main/resources/LambdaJsonLayout.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "timestamp": { - "$resolver": "lambda-timestamp" - }, - "instant": { - "epochSecond": { - "$resolver": "timestamp", - "epoch": { - "unit": "secs", - "rounded": true - } - }, - "nanoOfSecond": { - "$resolver": "timestamp", - "epoch": { - "unit": "secs.nanos" - } - } - }, - "thread": { - "$resolver": "thread", - "field": "name" - }, - "level": { - "$resolver": "level", - "field": "name" - }, - "loggerName": { - "$resolver": "logger", - "field": "name" - }, - "message": { - "$resolver": "message", - "stringified": true - }, - "thrown": { - "message": { - "$resolver": "exception", - "field": "message" - }, - "name": { - "$resolver": "exception", - "field": "className" - }, - "extendedStackTrace": { - "$resolver": "exception", - "field": "stackTrace" - } - }, - "contextStack": { - "$resolver": "ndc" - }, - "endOfBatch": { - "$resolver": "endOfBatch" - }, - "loggerFqcn": { - "$resolver": "logger", - "field": "fqcn" - }, - "threadId": { - "$resolver": "thread", - "field": "id" - }, - "threadPriority": { - "$resolver": "thread", - "field": "priority" - }, - "source": { - "class": { - "$resolver": "source", - "field": "className" - }, - "method": { - "$resolver": "source", - "field": "methodName" - }, - "file": { - "$resolver": "source", - "field": "fileName" - }, - "line": { - "$resolver": "source", - "field": "lineNumber" - } - }, - "": { - "$resolver": "powertools" - } -} diff --git a/powertools-logging/src/main/resources/log4j2.component.properties b/powertools-logging/src/main/resources/log4j2.component.properties deleted file mode 100644 index 3c392dd13..000000000 --- a/powertools-logging/src/main/resources/log4j2.component.properties +++ /dev/null @@ -1,2 +0,0 @@ -log4j.layout.jsonTemplate.timestampFormatPattern=yyyy-MM-dd'T'HH:mm:ss.SSSZz -#log4j.layout.jsonTemplate.timeZone= \ No newline at end of file diff --git a/powertools-logging/src/test/java/org/apache/logging/log4j/core/layout/LambdaJsonLayoutTest.java b/powertools-logging/src/test/java/org/apache/logging/log4j/core/layout/LambdaJsonLayoutTest.java deleted file mode 100644 index 95fb9c47f..000000000 --- a/powertools-logging/src/test/java/org/apache/logging/log4j/core/layout/LambdaJsonLayoutTest.java +++ /dev/null @@ -1,173 +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 org.apache.logging.log4j.core.layout; - -import static java.util.Collections.emptyMap; -import static org.apache.commons.lang3.reflect.FieldUtils.writeStaticField; -import static org.assertj.core.api.Assertions.as; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.openMocks; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.nio.channels.FileChannel; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.time.format.ResolverStyle; -import java.util.Map; -import org.apache.logging.log4j.Level; -import org.assertj.core.api.InstanceOfAssertFactories; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import software.amazon.lambda.powertools.logging.handlers.PowerLogToolEnabled; -import software.amazon.lambda.powertools.logging.handlers.PowerLogToolSamplingEnabled; -import software.amazon.lambda.powertools.logging.internal.LambdaLoggingAspect; - -class LambdaJsonLayoutTest { - private RequestHandler handler = new PowerLogToolEnabled(); - - @Mock - private Context context; - - @BeforeEach - void setUp() throws IOException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { - openMocks(this); - setupContext(); - //Make sure file is cleaned up before running full stack logging regression - FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); - resetLogLevel(Level.INFO); - } - - @Test - void shouldLogInStructuredFormat() throws IOException { - handler.handleRequest("test", context); - - assertThat(Files.lines(Paths.get("target/logfile.json"))) - .hasSize(1) - .allSatisfy(line -> assertThat(parseToMap(line)) - .containsEntry("functionName", "testFunction") - .containsEntry("functionVersion", "1") - .containsEntry("functionMemorySize", "10") - .containsEntry("functionArn", "testArn") - .containsKey("timestamp") - .containsKey("message") - .containsKey("service")); - } - - @Test - void shouldLogWithRFC3339TimestampFormat_WhenLambdaLoggingIsJSON() throws Exception { - // Given: AWS_LAMBDA_LOG_FORMAT=JSON defined in pom.xml - - // When - handler.handleRequest("test", context); - - // Then - assertThat(Files.lines(Paths.get("target/logfile.json"))) - .hasSize(1) - .allSatisfy(line -> assertThat(parseToMap(line)) - .extracting("timestamp", as(InstanceOfAssertFactories.STRING)) - .satisfies(s -> assertThat(hasDateFormat(s, "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")).isTrue())); - } - - private boolean hasDateFormat(String timestamp, String format) { - DateTimeFormatter dtf = DateTimeFormatter.ofPattern(format).withResolverStyle(ResolverStyle.STRICT); - try { - dtf.parse(timestamp); - return true; - } catch (DateTimeParseException e) { - return false; - } - } - - @Test - void shouldModifyLogLevelBasedOnEnvVariable() - throws IllegalAccessException, IOException, NoSuchMethodException, InvocationTargetException { - resetLogLevel(Level.DEBUG); - - handler.handleRequest("test", context); - - assertThat(Files.lines(Paths.get("target/logfile.json"))) - .hasSize(2) - .satisfies(line -> - { - assertThat(parseToMap(line.get(0))) - .containsEntry("level", "INFO") - .containsEntry("message", "Test event"); - - assertThat(parseToMap(line.get(1))) - .containsEntry("level", "DEBUG") - .containsEntry("message", "Test debug event"); - }); - } - - @Test - void shouldModifyLogLevelBasedOnSamplingRule() throws IOException { - handler = new PowerLogToolSamplingEnabled(); - - handler.handleRequest("test", context); - - assertThat(Files.lines(Paths.get("target/logfile.json"))) - .hasSize(3) - .satisfies(line -> - { - assertThat(parseToMap(line.get(0))) - .containsEntry("level", "DEBUG") - .containsEntry("loggerName", LambdaLoggingAspect.class.getCanonicalName()); - - assertThat(parseToMap(line.get(1))) - .containsEntry("level", "INFO") - .containsEntry("message", "Test event"); - - assertThat(parseToMap(line.get(2))) - .containsEntry("level", "DEBUG") - .containsEntry("message", "Test debug event"); - }); - } - - private void resetLogLevel(Level level) - throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { - Method resetLogLevels = LambdaLoggingAspect.class.getDeclaredMethod("resetLogLevels", Level.class); - resetLogLevels.setAccessible(true); - resetLogLevels.invoke(null, level); - writeStaticField(LambdaLoggingAspect.class, "LEVEL_AT_INITIALISATION", level, true); - } - - private Map parseToMap(String stringAsJson) { - try { - return new ObjectMapper().readValue(stringAsJson, Map.class); - } catch (JsonProcessingException e) { - fail("Failed parsing logger line " + stringAsJson); - return emptyMap(); - } - } - - private void setupContext() { - when(context.getFunctionName()).thenReturn("testFunction"); - when(context.getInvokedFunctionArn()).thenReturn("testArn"); - when(context.getFunctionVersion()).thenReturn("1"); - when(context.getMemoryLimitInMB()).thenReturn(10); - } -} \ No newline at end of file diff --git a/powertools-logging/src/test/java/org/slf4j/test/OutputChoice.java b/powertools-logging/src/test/java/org/slf4j/test/OutputChoice.java new file mode 100644 index 000000000..9a6d56b81 --- /dev/null +++ b/powertools-logging/src/test/java/org/slf4j/test/OutputChoice.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + *

+ * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + *

+ * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.slf4j.test; + +import java.io.PrintStream; + +/** + * This class encapsulates the user's choice of output target. + * + * @see ... + */ +class OutputChoice { + + final OutputChoiceType outputChoiceType; + final PrintStream targetPrintStream; + OutputChoice(OutputChoiceType outputChoiceType) { + if (outputChoiceType == OutputChoiceType.FILE) { + throw new IllegalArgumentException(); + } + this.outputChoiceType = outputChoiceType; + if (outputChoiceType == OutputChoiceType.CACHED_SYS_OUT) { + this.targetPrintStream = System.out; + } else if (outputChoiceType == OutputChoiceType.CACHED_SYS_ERR) { + this.targetPrintStream = System.err; + } else { + this.targetPrintStream = null; + } + } + + OutputChoice(PrintStream printStream) { + this.outputChoiceType = OutputChoiceType.FILE; + this.targetPrintStream = printStream; + } + + PrintStream getTargetPrintStream() { + switch (outputChoiceType) { + case SYS_OUT: + return System.out; + case SYS_ERR: + return System.err; + case CACHED_SYS_ERR: + case CACHED_SYS_OUT: + case FILE: + return targetPrintStream; + default: + throw new IllegalArgumentException(); + } + + } + + enum OutputChoiceType { + SYS_OUT, CACHED_SYS_OUT, SYS_ERR, CACHED_SYS_ERR, FILE; + } + +} diff --git a/powertools-logging/src/test/java/org/slf4j/test/TestLogger.java b/powertools-logging/src/test/java/org/slf4j/test/TestLogger.java new file mode 100644 index 000000000..2a9322592 --- /dev/null +++ b/powertools-logging/src/test/java/org/slf4j/test/TestLogger.java @@ -0,0 +1,452 @@ +/** + * Copyright (c) 2004-2022 QOS.ch Sarl (Switzerland) + * All rights reserved. + *

+ * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + *

+ * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.slf4j.test; + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.Marker; +import org.slf4j.event.Level; +import org.slf4j.event.LoggingEvent; +import org.slf4j.helpers.LegacyAbstractLogger; +import org.slf4j.helpers.MessageFormatter; +import org.slf4j.helpers.NormalizedParameters; +import org.slf4j.spi.LocationAwareLogger; + +/** + *

+ * Simple implementation of {@link Logger} that sends all enabled log messages, + * for all defined loggers, to the console ({@code System.err}). The following + * system properties are supported to configure the behavior of this logger: + * + * + *

    + *
  • org.slf4j.simpleLogger.logFile - The output target which can + * be the path to a file, or the special values "System.out" and + * "System.err". Default is "System.err".
  • + * + *
  • org.slf4j.simpleLogger.cacheOutputStream - If the output + * target is set to "System.out" or "System.err" (see preceding entry), by + * default, logs will be output to the latest value referenced by + * System.out/err variables. By setting this parameter to true, the + * output stream will be cached, i.e. assigned once at initialization time and + * re-used independently of the current value referenced by + * System.out/err.
  • + * + *
  • org.slf4j.simpleLogger.defaultLogLevel - Default log level + * for all instances of SimpleLogger. Must be one of ("trace", "debug", "info", + * "warn", "error" or "off"). If not specified, defaults to "info".
  • + * + *
  • org.slf4j.simpleLogger.log.a.b.c - Logging detail + * level for a SimpleLogger instance named "a.b.c". Right-side value must be one + * of "trace", "debug", "info", "warn", "error" or "off". When a SimpleLogger + * named "a.b.c" is initialized, its level is assigned from this property. If + * unspecified, the level of nearest parent logger will be used, and if none is + * set, then the value specified by + * org.slf4j.simpleLogger.defaultLogLevel will be used.
  • + * + *
  • org.slf4j.simpleLogger.showDateTime - Set to + * true if you want the current date and time to be included in + * output messages. Default is false
  • + * + *
  • org.slf4j.simpleLogger.dateTimeFormat - The date and time + * format to be used in the output messages. The pattern describing the date and + * time format is defined by + * SimpleDateFormat. If the format is not specified or is + * invalid, the number of milliseconds since start up will be output.
  • + * + *
  • org.slf4j.simpleLogger.showThreadName -Set to + * true if you want to output the current thread name. Defaults to + * true.
  • + * + *
  • (since version 1.7.33 and 2.0.0-alpha6) org.slf4j.simpleLogger.showThreadId - + * If you would like to output the current thread id, then set to + * true. Defaults to false.
  • + * + *
  • org.slf4j.simpleLogger.showLogName - Set to + * true if you want the Logger instance name to be included in + * output messages. Defaults to true.
  • + * + *
  • org.slf4j.simpleLogger.showShortLogName - Set to + * true if you want the last component of the name to be included + * in output messages. Defaults to false.
  • + * + *
  • org.slf4j.simpleLogger.levelInBrackets - Should the level + * string be output in brackets? Defaults to false.
  • + * + *
  • org.slf4j.simpleLogger.warnLevelString - The string value + * output for the warn level. Defaults to WARN.
  • + * + *
+ * + *

+ * In addition to looking for system properties with the names specified above, + * this implementation also checks for a class loader resource named + * "simplelogger.properties", and includes any matching definitions + * from this resource (if it exists). + * + * + *

+ * With no configuration, the default output includes the relative time in + * milliseconds, thread name, the level, logger name, and the message followed + * by the line separator for the host. In log4j terms it amounts to the "%r [%t] + * %level %logger - %m%n" pattern. + * + *

+ * Sample output follows. + * + * + *

+ * 176 [main] INFO examples.Sort - Populating an array of 2 elements in reverse order.
+ * 225 [main] INFO examples.SortAlgo - Entered the sort method.
+ * 304 [main] INFO examples.SortAlgo - Dump of integer array:
+ * 317 [main] INFO examples.SortAlgo - Element [0] = 0
+ * 331 [main] INFO examples.SortAlgo - Element [1] = 1
+ * 343 [main] INFO examples.Sort - The next log statement should be an error message.
+ * 346 [main] ERROR examples.SortAlgo - Tried to dump an uninitialized array.
+ *   at org.log4j.examples.SortAlgo.dump(SortAlgo.java:58)
+ *   at org.log4j.examples.Sort.main(Sort.java:64)
+ * 467 [main] INFO  examples.Sort - Exiting main method.
+ * 
+ * + *

+ * This implementation is heavily inspired by + * Apache Commons Logging's + * SimpleLog. + * + * + * @author Ceki Gülcü + * @author Scott Sanders + * @author Rod Waldhoff + * @author Robert Burrell Donkin + * @author Cédrik LIME + */ +public class TestLogger extends LegacyAbstractLogger { + + /** + * All system properties used by SimpleLogger start with this + * prefix + */ + public static final String SYSTEM_PREFIX = "org.slf4j.simpleLogger."; + public static final String LOG_KEY_PREFIX = TestLogger.SYSTEM_PREFIX + "log."; + public static final String CACHE_OUTPUT_STREAM_STRING_KEY = TestLogger.SYSTEM_PREFIX + "cacheOutputStream"; + public static final String WARN_LEVEL_STRING_KEY = TestLogger.SYSTEM_PREFIX + "warnLevelString"; + public static final String LEVEL_IN_BRACKETS_KEY = TestLogger.SYSTEM_PREFIX + "levelInBrackets"; + public static final String LOG_FILE_KEY = TestLogger.SYSTEM_PREFIX + "logFile"; + public static final String SHOW_SHORT_LOG_NAME_KEY = TestLogger.SYSTEM_PREFIX + "showShortLogName"; + public static final String SHOW_LOG_NAME_KEY = TestLogger.SYSTEM_PREFIX + "showLogName"; + public static final String SHOW_THREAD_NAME_KEY = TestLogger.SYSTEM_PREFIX + "showThreadName"; + public static final String SHOW_THREAD_ID_KEY = TestLogger.SYSTEM_PREFIX + "showThreadId"; + public static final String DATE_TIME_FORMAT_KEY = TestLogger.SYSTEM_PREFIX + "dateTimeFormat"; + public static final String SHOW_DATE_TIME_KEY = TestLogger.SYSTEM_PREFIX + "showDateTime"; + public static final String DEFAULT_LOG_LEVEL_KEY = TestLogger.SYSTEM_PREFIX + "defaultLogLevel"; + protected static final int LOG_LEVEL_TRACE = LocationAwareLogger.TRACE_INT; + protected static final int LOG_LEVEL_DEBUG = LocationAwareLogger.DEBUG_INT; + protected static final int LOG_LEVEL_INFO = LocationAwareLogger.INFO_INT; + protected static final int LOG_LEVEL_WARN = LocationAwareLogger.WARN_INT; + protected static final int LOG_LEVEL_ERROR = LocationAwareLogger.ERROR_INT; + // The OFF level can only be used in configuration files to disable logging. + // It has + // no printing method associated with it in o.s.Logger interface. + protected static final int LOG_LEVEL_OFF = LOG_LEVEL_ERROR + 10; + static final String TID_PREFIX = "tid="; + static final TestLoggerConfiguration CONFIG_PARAMS = new TestLoggerConfiguration(); + private static final long serialVersionUID = -632788891211436180L; + private static final long START_TIME = System.currentTimeMillis(); + static char SP = ' '; + private static boolean INITIALIZED = false; + /** The current log level */ + protected int currentLogLevel = LOG_LEVEL_INFO; + /** The short name of this simple log instance */ + private transient String shortLogName = null; + + /** + * Package access allows only {@link TestLoggerFactory} to instantiate + * SimpleLogger instances. + */ + TestLogger(String name) { + this.name = name; + + String levelString = recursivelyComputeLevelString(); + if (levelString != null) { + this.currentLogLevel = TestLoggerConfiguration.stringToLevel(levelString); + } else { + this.currentLogLevel = CONFIG_PARAMS.defaultLogLevel; + } + } + + static void lazyInit() { + if (INITIALIZED) { + return; + } + INITIALIZED = true; + init(); + } + + // external software might be invoking this method directly. Do not rename + // or change its semantics. + static void init() { + CONFIG_PARAMS.init(); + } + + public int getLogLevel() { + return currentLogLevel; + } + + public void setLogLevel(String levelString) { + this.currentLogLevel = TestLoggerConfiguration.stringToLevel(levelString); + } + + String recursivelyComputeLevelString() { + String tempName = name; + String levelString = null; + int indexOfLastDot = tempName.length(); + while ((levelString == null) && (indexOfLastDot > -1)) { + tempName = tempName.substring(0, indexOfLastDot); + levelString = CONFIG_PARAMS.getStringProperty(TestLogger.LOG_KEY_PREFIX + tempName, null); + indexOfLastDot = String.valueOf(tempName).lastIndexOf("."); + } + return levelString; + } + + /** + * To avoid intermingling of log messages and associated stack traces, the two + * operations are done in a synchronized block. + * + * @param buf + * @param t + */ + void write(StringBuilder buf, Throwable t) { + PrintStream targetStream = CONFIG_PARAMS.outputChoice.getTargetPrintStream(); + + synchronized (CONFIG_PARAMS) { + targetStream.println(buf.toString()); + writeThrowable(t, targetStream); + targetStream.flush(); + } + + } + + protected void writeThrowable(Throwable t, PrintStream targetStream) { + if (t != null) { + t.printStackTrace(targetStream); + } + } + + private String getFormattedDate() { + Date now = new Date(); + String dateText; + synchronized (CONFIG_PARAMS.dateFormatter) { + dateText = CONFIG_PARAMS.dateFormatter.format(now); + } + return dateText; + } + + private String computeShortName() { + return name.substring(name.lastIndexOf(".") + 1); + } + + // /** + // * For formatted messages, first substitute arguments and then log. + // * + // * @param level + // * @param format + // * @param arg1 + // * @param arg2 + // */ + // private void formatAndLog(int level, String format, Object arg1, Object arg2) { + // if (!isLevelEnabled(level)) { + // return; + // } + // FormattingTuple tp = MessageFormatter.format(format, arg1, arg2); + // log(level, tp.getMessage(), tp.getThrowable()); + // } + + // /** + // * For formatted messages, first substitute arguments and then log. + // * + // * @param level + // * @param format + // * @param arguments + // * a list of 3 ore more arguments + // */ + // private void formatAndLog(int level, String format, Object... arguments) { + // if (!isLevelEnabled(level)) { + // return; + // } + // FormattingTuple tp = MessageFormatter.arrayFormat(format, arguments); + // log(level, tp.getMessage(), tp.getThrowable()); + // } + + /** + * Is the given log level currently enabled? + * + * @param logLevel is this level enabled? + * @return whether the logger is enabled for the given level + */ + protected boolean isLevelEnabled(int logLevel) { + // log level are numerically ordered so can use simple numeric + // comparison + return (logLevel >= currentLogLevel); + } + + /** Are {@code trace} messages currently enabled? */ + public boolean isTraceEnabled() { + return isLevelEnabled(LOG_LEVEL_TRACE); + } + + /** Are {@code debug} messages currently enabled? */ + public boolean isDebugEnabled() { + return isLevelEnabled(LOG_LEVEL_DEBUG); + } + + /** Are {@code info} messages currently enabled? */ + public boolean isInfoEnabled() { + return isLevelEnabled(LOG_LEVEL_INFO); + } + + /** Are {@code warn} messages currently enabled? */ + public boolean isWarnEnabled() { + return isLevelEnabled(LOG_LEVEL_WARN); + } + + /** Are {@code error} messages currently enabled? */ + public boolean isErrorEnabled() { + return isLevelEnabled(LOG_LEVEL_ERROR); + } + + /** + * SimpleLogger's implementation of + * {@link org.slf4j.helpers.AbstractLogger#handleNormalizedLoggingCall(Level, Marker, String, Object[], Throwable) AbstractLogger#handleNormalizedLoggingCall} + * } + * + * @param level the SLF4J level for this event + * @param marker The marker to be used for this event, may be null. + * @param messagePattern The message pattern which will be parsed and formatted + * @param arguments the array of arguments to be formatted, may be null + * @param throwable The exception whose stack trace should be logged, may be null + */ + @Override + protected void handleNormalizedLoggingCall(Level level, Marker marker, String messagePattern, Object[] arguments, + Throwable throwable) { + + List markers = null; + + if (marker != null) { + markers = new ArrayList<>(); + markers.add(marker); + } + + innerHandleNormalizedLoggingCall(level, markers, messagePattern, arguments, throwable); + } + + private void innerHandleNormalizedLoggingCall(Level level, List markers, String messagePattern, + Object[] arguments, Throwable t) { + + StringBuilder buf = new StringBuilder(32); + + // Append date-time if so configured + if (CONFIG_PARAMS.showDateTime) { + if (CONFIG_PARAMS.dateFormatter != null) { + buf.append(getFormattedDate()); + buf.append(SP); + } else { + buf.append(System.currentTimeMillis() - START_TIME); + buf.append(SP); + } + } + + // Append current thread name if so configured + if (CONFIG_PARAMS.showThreadName) { + buf.append('['); + buf.append(Thread.currentThread().getName()); + buf.append("] "); + } + + if (CONFIG_PARAMS.showThreadId) { + buf.append(TID_PREFIX); + buf.append(Thread.currentThread().getId()); + buf.append(SP); + } + + if (CONFIG_PARAMS.levelInBrackets) { + buf.append('['); + } + + // Append a readable representation of the log level + String levelStr = level.name(); + buf.append(levelStr); + if (CONFIG_PARAMS.levelInBrackets) { + buf.append(']'); + } + buf.append(SP); + + // Append the name of the log instance if so configured + if (CONFIG_PARAMS.showShortLogName) { + if (shortLogName == null) { + shortLogName = computeShortName(); + } + buf.append(String.valueOf(shortLogName)).append(" - "); + } else if (CONFIG_PARAMS.showLogName) { + buf.append(String.valueOf(name)).append(" - "); + } + + if (markers != null) { + buf.append(SP); + for (Marker marker : markers) { + buf.append(marker.getName()).append(SP); + } + } + + String formattedMessage = MessageFormatter.basicArrayFormat(messagePattern, arguments); + + // Append the message + buf.append(formattedMessage); + + write(buf, t); + } + + public void log(LoggingEvent event) { + int levelInt = event.getLevel().toInt(); + + if (!isLevelEnabled(levelInt)) { + return; + } + + NormalizedParameters np = NormalizedParameters.normalize(event); + + innerHandleNormalizedLoggingCall(event.getLevel(), event.getMarkers(), np.getMessage(), np.getArguments(), + event.getThrowable()); + } + + @Override + protected String getFullyQualifiedCallerName() { + return null; + } + +} diff --git a/powertools-logging/src/test/java/org/slf4j/test/TestLoggerConfiguration.java b/powertools-logging/src/test/java/org/slf4j/test/TestLoggerConfiguration.java new file mode 100644 index 000000000..7601dbfde --- /dev/null +++ b/powertools-logging/src/test/java/org/slf4j/test/TestLoggerConfiguration.java @@ -0,0 +1,204 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + *

+ * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + *

+ * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.slf4j.test; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.PrintStream; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Properties; +import org.slf4j.helpers.Util; + +/** + * This class holds configuration values for {@link TestLogger}. The + * values are computed at runtime. See {@link TestLogger} documentation for + * more information. + * + * + * @author Ceki Gülcü + * @author Scott Sanders + * @author Rod Waldhoff + * @author Robert Burrell Donkin + * @author Cédrik LIME + * + * @since 1.7.25 + */ +public class TestLoggerConfiguration { + + final static boolean SHOW_LOG_NAME_DEFAULT = true; + private static final String CONFIGURATION_FILE = "testlogger.properties"; + private static final boolean SHOW_DATE_TIME_DEFAULT = false; + private static final String DATE_TIME_FORMAT_STR_DEFAULT = null; + private static final boolean SHOW_THREAD_NAME_DEFAULT = true; + /** + * See https://jira.qos.ch/browse/SLF4J-499 + * @since 1.7.33 and 2.0.0-alpha6 + */ + private static final boolean SHOW_THREAD_ID_DEFAULT = false; + private static final boolean SHOW_SHORT_LOG_NAME_DEFAULT = false; + private static final boolean LEVEL_IN_BRACKETS_DEFAULT = false; + private static final String LOG_FILE_DEFAULT = "System.err"; + private static final boolean CACHE_OUTPUT_STREAM_DEFAULT = false; + private static final String WARN_LEVELS_STRING_DEFAULT = "WARN"; + static int DEFAULT_LOG_LEVEL_DEFAULT = TestLogger.LOG_LEVEL_INFO; + private static String dateTimeFormatStr = DATE_TIME_FORMAT_STR_DEFAULT; + private final Properties properties = new Properties(); + int defaultLogLevel = DEFAULT_LOG_LEVEL_DEFAULT; + boolean showDateTime = SHOW_DATE_TIME_DEFAULT; + DateFormat dateFormatter = null; + boolean showThreadName = SHOW_THREAD_NAME_DEFAULT; + boolean showThreadId = SHOW_THREAD_ID_DEFAULT; + boolean showLogName = SHOW_LOG_NAME_DEFAULT; + boolean showShortLogName = SHOW_SHORT_LOG_NAME_DEFAULT; + boolean levelInBrackets = LEVEL_IN_BRACKETS_DEFAULT; + OutputChoice outputChoice = null; + String warnLevelString = WARN_LEVELS_STRING_DEFAULT; + private String logFile = LOG_FILE_DEFAULT; + private boolean cacheOutputStream = CACHE_OUTPUT_STREAM_DEFAULT; + + static int stringToLevel(String levelStr) { + if ("trace".equalsIgnoreCase(levelStr)) { + return TestLogger.LOG_LEVEL_TRACE; + } else if ("debug".equalsIgnoreCase(levelStr)) { + return TestLogger.LOG_LEVEL_DEBUG; + } else if ("info".equalsIgnoreCase(levelStr)) { + return TestLogger.LOG_LEVEL_INFO; + } else if ("warn".equalsIgnoreCase(levelStr)) { + return TestLogger.LOG_LEVEL_WARN; + } else if ("error".equalsIgnoreCase(levelStr)) { + return TestLogger.LOG_LEVEL_ERROR; + } else if ("off".equalsIgnoreCase(levelStr)) { + return TestLogger.LOG_LEVEL_OFF; + } + // assume INFO by default + return TestLogger.LOG_LEVEL_INFO; + } + + private static OutputChoice computeOutputChoice(String logFile, boolean cacheOutputStream) { + if ("System.err".equalsIgnoreCase(logFile)) { + if (cacheOutputStream) { + return new OutputChoice(OutputChoice.OutputChoiceType.CACHED_SYS_ERR); + } else { + return new OutputChoice(OutputChoice.OutputChoiceType.SYS_ERR); + } + } else if ("System.out".equalsIgnoreCase(logFile)) { + if (cacheOutputStream) { + return new OutputChoice(OutputChoice.OutputChoiceType.CACHED_SYS_OUT); + } else { + return new OutputChoice(OutputChoice.OutputChoiceType.SYS_OUT); + } + } else { + try { + FileOutputStream fos = new FileOutputStream(logFile); + PrintStream printStream = new PrintStream(fos); + return new OutputChoice(printStream); + } catch (FileNotFoundException e) { + Util.report("Could not open [" + logFile + "]. Defaulting to System.err", e); + return new OutputChoice(OutputChoice.OutputChoiceType.SYS_ERR); + } + } + } + + void init() { + loadProperties(); + + String defaultLogLevelString = getStringProperty(TestLogger.DEFAULT_LOG_LEVEL_KEY, null); + if (defaultLogLevelString != null) { + defaultLogLevel = stringToLevel(defaultLogLevelString); + } + + showLogName = getBooleanProperty(TestLogger.SHOW_LOG_NAME_KEY, TestLoggerConfiguration.SHOW_LOG_NAME_DEFAULT); + showShortLogName = getBooleanProperty(TestLogger.SHOW_SHORT_LOG_NAME_KEY, SHOW_SHORT_LOG_NAME_DEFAULT); + showDateTime = getBooleanProperty(TestLogger.SHOW_DATE_TIME_KEY, SHOW_DATE_TIME_DEFAULT); + showThreadName = getBooleanProperty(TestLogger.SHOW_THREAD_NAME_KEY, SHOW_THREAD_NAME_DEFAULT); + showThreadId = getBooleanProperty(TestLogger.SHOW_THREAD_ID_KEY, SHOW_THREAD_ID_DEFAULT); + dateTimeFormatStr = getStringProperty(TestLogger.DATE_TIME_FORMAT_KEY, DATE_TIME_FORMAT_STR_DEFAULT); + levelInBrackets = getBooleanProperty(TestLogger.LEVEL_IN_BRACKETS_KEY, LEVEL_IN_BRACKETS_DEFAULT); + warnLevelString = getStringProperty(TestLogger.WARN_LEVEL_STRING_KEY, WARN_LEVELS_STRING_DEFAULT); + + logFile = getStringProperty(TestLogger.LOG_FILE_KEY, logFile); + + cacheOutputStream = getBooleanProperty(TestLogger.CACHE_OUTPUT_STREAM_STRING_KEY, CACHE_OUTPUT_STREAM_DEFAULT); + outputChoice = computeOutputChoice(logFile, cacheOutputStream); + + if (dateTimeFormatStr != null) { + try { + dateFormatter = new SimpleDateFormat(dateTimeFormatStr); + } catch (IllegalArgumentException e) { + Util.report("Bad date format in " + CONFIGURATION_FILE + "; will output relative time", e); + } + } + } + + private void loadProperties() { + // Add props from the resource testlogger.properties + InputStream in = AccessController.doPrivileged((PrivilegedAction) () -> { + ClassLoader threadCL = Thread.currentThread().getContextClassLoader(); + if (threadCL != null) { + return threadCL.getResourceAsStream(CONFIGURATION_FILE); + } else { + return ClassLoader.getSystemResourceAsStream(CONFIGURATION_FILE); + } + }); + if (null != in) { + try { + properties.load(in); + } catch (java.io.IOException e) { + // ignored + } finally { + try { + in.close(); + } catch (java.io.IOException e) { + // ignored + } + } + } + } + + String getStringProperty(String name, String defaultValue) { + String prop = getStringProperty(name); + return (prop == null) ? defaultValue : prop; + } + + boolean getBooleanProperty(String name, boolean defaultValue) { + String prop = getStringProperty(name); + return (prop == null) ? defaultValue : "true".equalsIgnoreCase(prop); + } + + String getStringProperty(String name) { + String prop = null; + try { + prop = System.getProperty(name); + } catch (SecurityException e) { + ; // Ignore + } + return (prop == null) ? properties.getProperty(name) : prop; + } + +} diff --git a/powertools-logging/src/test/java/org/slf4j/test/TestLoggerFactory.java b/powertools-logging/src/test/java/org/slf4j/test/TestLoggerFactory.java new file mode 100644 index 000000000..d597b5706 --- /dev/null +++ b/powertools-logging/src/test/java/org/slf4j/test/TestLoggerFactory.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + *

+ * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + *

+ * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.slf4j.test; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.slf4j.ILoggerFactory; +import org.slf4j.Logger; + +/** + * An implementation of {@link ILoggerFactory} which always returns + * {@link TestLogger} instances. + * + * @author Ceki Gülcü + */ +public class TestLoggerFactory implements ILoggerFactory { + + ConcurrentMap loggerMap; + + public TestLoggerFactory() { + loggerMap = new ConcurrentHashMap<>(); + TestLogger.lazyInit(); + } + + public Map getLoggers() { + return loggerMap; + } + + /** + * Return an appropriate {@link TestLogger} instance by name. + */ + public Logger getLogger(String name) { + Logger simpleLogger = loggerMap.get(name); + if (simpleLogger != null) { + return simpleLogger; + } else { + Logger newInstance = new TestLogger(name); + Logger oldInstance = loggerMap.putIfAbsent(name, newInstance); + return oldInstance == null ? newInstance : oldInstance; + } + } + + /** + * Clear the internal logger cache. + * + * This method is intended to be called by classes (in the same package) for + * testing purposes. This method is internal. It can be modified, renamed or + * removed at any time without notice. + * + * You are strongly discouraged from calling this method in production code. + */ + void reset() { + loggerMap.clear(); + } +} diff --git a/powertools-logging/src/test/java/org/slf4j/test/TestServiceProvider.java b/powertools-logging/src/test/java/org/slf4j/test/TestServiceProvider.java new file mode 100644 index 000000000..357360d1e --- /dev/null +++ b/powertools-logging/src/test/java/org/slf4j/test/TestServiceProvider.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + *

+ * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + *

+ * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.slf4j.test; + +import org.slf4j.ILoggerFactory; +import org.slf4j.IMarkerFactory; +import org.slf4j.helpers.BasicMDCAdapter; +import org.slf4j.helpers.BasicMarkerFactory; +import org.slf4j.spi.MDCAdapter; +import org.slf4j.spi.SLF4JServiceProvider; + +/** + * Copy of the org.slf4j.simple.SimpleServiceProvider, replacing the NoOpMDCAdapter with a BasicMDCAdapter to test the MDC + */ +public class TestServiceProvider implements SLF4JServiceProvider { + /** + * Declare the version of the SLF4J API this implementation is compiled against. + * The value of this field is modified with each major release. + */ + // to avoid constant folding by the compiler, this field must *not* be final + public static String REQUESTED_API_VERSION = "2.0.99"; // !final + + private ILoggerFactory loggerFactory; + private IMarkerFactory markerFactory; + private MDCAdapter mdcAdapter; + + public ILoggerFactory getLoggerFactory() { + return loggerFactory; + } + + @Override + public IMarkerFactory getMarkerFactory() { + return markerFactory; + } + + @Override + public MDCAdapter getMDCAdapter() { + return mdcAdapter; + } + + @Override + public String getRequestedApiVersion() { + return REQUESTED_API_VERSION; + } + + @Override + public void initialize() { + + loggerFactory = new TestLoggerFactory(); + markerFactory = new BasicMarkerFactory(); + mdcAdapter = new BasicMDCAdapter(); + } + +} 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 index 8889fb93c..04e977c58 100644 --- 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 @@ -16,73 +16,99 @@ 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.apache.logging.log4j.ThreadContext; 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() { - ThreadContext.clearAll(); + MDC.clear(); } @Test - void shouldSetCustomKeyOnThreadContext() { - LoggingUtils.appendKey("test", "value"); + void shouldSetCustomKeyInLoggingContext() { + LoggingUtils.appendKey("org/slf4j/test", "value"); - assertThat(ThreadContext.getImmutableContext()) + assertThat(MDC.getCopyOfContextMap()) .hasSize(1) - .containsEntry("test", "value"); + .containsEntry("org/slf4j/test", "value"); } @Test - void shouldSetCustomKeyAsMapOnThreadContext() { + void shouldSetCustomKeyAsMapInLoggingContext() { Map customKeys = new HashMap<>(); - customKeys.put("test", "value"); + customKeys.put("org/slf4j/test", "value"); customKeys.put("test1", "value1"); LoggingUtils.appendKeys(customKeys); - assertThat(ThreadContext.getImmutableContext()) + assertThat(MDC.getCopyOfContextMap()) .hasSize(2) - .containsEntry("test", "value") + .containsEntry("org/slf4j/test", "value") .containsEntry("test1", "value1"); } @Test - void shouldRemoveCustomKeyOnThreadContext() { - LoggingUtils.appendKey("test", "value"); + void shouldRemoveCustomKeyInLoggingContext() { + LoggingUtils.appendKey("org/slf4j/test", "value"); - assertThat(ThreadContext.getImmutableContext()) + assertThat(MDC.getCopyOfContextMap()) .hasSize(1) - .containsEntry("test", "value"); + .containsEntry("org/slf4j/test", "value"); - LoggingUtils.removeKey("test"); + LoggingUtils.removeKey("org/slf4j/test"); - assertThat(ThreadContext.getImmutableContext()) + assertThat(MDC.getCopyOfContextMap()) .isEmpty(); } @Test - void shouldRemoveCustomKeysOnThreadContext() { + void shouldRemoveCustomKeysInLoggingContext() { Map customKeys = new HashMap<>(); - customKeys.put("test", "value"); + customKeys.put("org/slf4j/test", "value"); customKeys.put("test1", "value1"); LoggingUtils.appendKeys(customKeys); - assertThat(ThreadContext.getImmutableContext()) + assertThat(MDC.getCopyOfContextMap()) .hasSize(2) - .containsEntry("test", "value") + .containsEntry("org/slf4j/test", "value") .containsEntry("test1", "value1"); - LoggingUtils.removeKeys("test", "test1"); + LoggingUtils.removeKeys("org/slf4j/test", "test1"); - assertThat(ThreadContext.getImmutableContext()) + 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/handlers/PowerToolLogEventEnabledWithCustomMapper.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabledWithCustomMapper.java deleted file mode 100644 index 838de1216..000000000 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabledWithCustomMapper.java +++ /dev/null @@ -1,63 +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.handlers; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import java.io.IOException; -import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.logging.LoggingUtils; - -public class PowerToolLogEventEnabledWithCustomMapper implements RequestHandler { - - static { - ObjectMapper objectMapper = new ObjectMapper(); - SimpleModule module = new SimpleModule(); - module.addSerializer(S3EventNotification.class, new S3EventNotificationSerializer()); - objectMapper.registerModule(module); - LoggingUtils.defaultObjectMapper(objectMapper); - } - - @Logging(logEvent = true) - @Override - public Object handleRequest(S3EventNotification input, Context context) { - return null; - } - - static class S3EventNotificationSerializer extends StdSerializer { - - public S3EventNotificationSerializer() { - this(null); - } - - public S3EventNotificationSerializer(Class t) { - super(t); - } - - @Override - public void serialize(S3EventNotification o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) - throws IOException { - jsonGenerator.writeStartObject(); - jsonGenerator.writeStringField("eventSource", o.getRecords().get(0).getEventSource()); - jsonGenerator.writeEndObject(); - } - } -} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogAlbCorrelationId.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogAlbCorrelationId.java index a32e3e06e..065d4c5b0 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogAlbCorrelationId.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogAlbCorrelationId.java @@ -14,17 +14,17 @@ package software.amazon.lambda.powertools.logging.handlers; -import static software.amazon.lambda.powertools.logging.CorrelationIdPathConstants.APPLICATION_LOAD_BALANCER; +import static software.amazon.lambda.powertools.logging.CorrelationIdPaths.APPLICATION_LOAD_BALANCER; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.logging.Logging; public class PowertoolsLogAlbCorrelationId implements RequestHandler { - private final Logger LOG = LogManager.getLogger(PowertoolsLogAlbCorrelationId.class); + private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogAlbCorrelationId.class); @Override @Logging(correlationIdPath = APPLICATION_LOAD_BALANCER) diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolApiGatewayHttpApiCorrelationId.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogApiGatewayHttpApiCorrelationId.java similarity index 78% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolApiGatewayHttpApiCorrelationId.java rename to powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogApiGatewayHttpApiCorrelationId.java index 54d87d5cb..922a09f13 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolApiGatewayHttpApiCorrelationId.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogApiGatewayHttpApiCorrelationId.java @@ -14,17 +14,17 @@ package software.amazon.lambda.powertools.logging.handlers; -import static software.amazon.lambda.powertools.logging.CorrelationIdPathConstants.API_GATEWAY_HTTP; +import static software.amazon.lambda.powertools.logging.CorrelationIdPaths.API_GATEWAY_HTTP; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.logging.Logging; -public class PowerLogToolApiGatewayHttpApiCorrelationId implements RequestHandler { - private final Logger LOG = LogManager.getLogger(PowerLogToolApiGatewayHttpApiCorrelationId.class); +public class PowertoolsLogApiGatewayHttpApiCorrelationId implements RequestHandler { + private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogApiGatewayHttpApiCorrelationId.class); @Override @Logging(correlationIdPath = API_GATEWAY_HTTP) diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolApiGatewayRestApiCorrelationId.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogApiGatewayRestApiCorrelationId.java similarity index 78% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolApiGatewayRestApiCorrelationId.java rename to powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogApiGatewayRestApiCorrelationId.java index 2b6e5a8d4..7271e1d24 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolApiGatewayRestApiCorrelationId.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogApiGatewayRestApiCorrelationId.java @@ -14,17 +14,17 @@ package software.amazon.lambda.powertools.logging.handlers; -import static software.amazon.lambda.powertools.logging.CorrelationIdPathConstants.API_GATEWAY_REST; +import static software.amazon.lambda.powertools.logging.CorrelationIdPaths.API_GATEWAY_REST; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.logging.Logging; -public class PowerLogToolApiGatewayRestApiCorrelationId implements RequestHandler { - private final Logger LOG = LogManager.getLogger(PowerLogToolApiGatewayRestApiCorrelationId.class); +public class PowertoolsLogApiGatewayRestApiCorrelationId implements RequestHandler { + private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogApiGatewayRestApiCorrelationId.class); @Override @Logging(correlationIdPath = API_GATEWAY_REST) diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogAppSyncCorrelationId.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogAppSyncCorrelationId.java new file mode 100644 index 000000000..fbe2bc89b --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogAppSyncCorrelationId.java @@ -0,0 +1,37 @@ +/* + * 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 static software.amazon.lambda.powertools.logging.CorrelationIdPaths.APPSYNC_RESOLVER; + +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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.lambda.powertools.logging.Logging; + +public class PowertoolsLogAppSyncCorrelationId implements RequestStreamHandler { + + private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogAppSyncCorrelationId.class); + + @Override + @Logging(correlationIdPath = APPSYNC_RESOLVER) + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { + LOG.info("Test event"); + } +} \ No newline at end of file diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabledWithClearState.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogClearState.java similarity index 67% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabledWithClearState.java rename to powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogClearState.java index f21d9f118..e1829a777 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabledWithClearState.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogClearState.java @@ -16,23 +16,20 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.logging.Logging; import software.amazon.lambda.powertools.logging.LoggingUtils; -public class PowertoolsLogEnabledWithClearState implements RequestHandler { - private static final Logger LOG = LogManager.getLogger(PowertoolsLogEnabledWithClearState.class); - public static int COUNT = 1; +public class PowertoolsLogClearState implements RequestHandler, Object> { + private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogClearState.class); @Override @Logging(clearState = true) - public Object handleRequest(Object input, Context context) { - if (COUNT == 1) { - LoggingUtils.appendKey("TestKey", "TestValue"); - } + public Object handleRequest(Map input, Context context) { + LoggingUtils.appendKey("mySuperSecret", input.get("mySuperSecret")); LOG.info("Test event"); - COUNT++; return null; } } diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolDisabled.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogDisabled.java similarity index 91% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolDisabled.java rename to powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogDisabled.java index 48a2e3b81..54e887e40 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolDisabled.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogDisabled.java @@ -17,7 +17,7 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; -public class PowerToolDisabled implements RequestHandler { +public class PowertoolsLogDisabled implements RequestHandler { @Override public Object handleRequest(Object input, Context context) { diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolDisabledForStream.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogDisabledForStream.java similarity index 92% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolDisabledForStream.java rename to powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogDisabledForStream.java index 7f93145c7..7f7418ed6 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolDisabledForStream.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogDisabledForStream.java @@ -19,7 +19,7 @@ import java.io.InputStream; import java.io.OutputStream; -public class PowerToolDisabledForStream implements RequestStreamHandler { +public class PowertoolsLogDisabledForStream implements RequestStreamHandler { @Override public void handleRequest(InputStream input, OutputStream output, Context context) { 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 new file mode 100644 index 000000000..d6c79a445 --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabled.java @@ -0,0 +1,52 @@ +/* + * 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 PowertoolsLogEnabled implements RequestHandler { + private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogEnabled.class); + private final boolean throwError; + + public PowertoolsLogEnabled(boolean throwError) { + this.throwError = throwError; + } + + public PowertoolsLogEnabled() { + this(false); + } + + @Override + @Logging + public Object handleRequest(Object input, Context context) { + if (throwError) { + throw new RuntimeException("Something went wrong"); + } + LOG.error("Test error event"); + LOG.warn("Test warn event"); + LOG.info("Test event"); + LOG.debug("Test debug event"); + return "Bonjour le monde"; + } + + @Logging + public void anotherMethod() { + System.out.println("test"); + } +} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabledForStream.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabledForStream.java similarity index 93% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabledForStream.java rename to powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabledForStream.java index 83a370437..c95627302 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabledForStream.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEnabledForStream.java @@ -20,7 +20,7 @@ import java.io.OutputStream; import software.amazon.lambda.powertools.logging.Logging; -public class PowerLogToolEnabledForStream implements RequestStreamHandler { +public class PowertoolsLogEnabledForStream implements RequestStreamHandler { @Logging @Override diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogError.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogError.java new file mode 100644 index 000000000..a6b1ed915 --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogError.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.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import software.amazon.lambda.powertools.logging.Logging; + +public class PowertoolsLogError implements RequestHandler { + + @Override + @Logging(logError = true) + public Object handleRequest(Object input, Context context) { + throw new UnsupportedOperationException("This is an error"); + } +} diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabled.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEvent.java similarity index 92% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabled.java rename to powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEvent.java index 8a960fa87..87677d601 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabled.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEvent.java @@ -18,10 +18,10 @@ import com.amazonaws.services.lambda.runtime.RequestHandler; import software.amazon.lambda.powertools.logging.Logging; -public class PowerToolLogEventEnabled implements RequestHandler { +public class PowertoolsLogEvent implements RequestHandler { - @Logging(logEvent = true) @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/PowertoolsLogEventBridgeCorrelationId.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventBridgeCorrelationId.java index 53e06cb2e..04d56d38c 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventBridgeCorrelationId.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventBridgeCorrelationId.java @@ -14,20 +14,20 @@ package software.amazon.lambda.powertools.logging.handlers; -import static software.amazon.lambda.powertools.logging.CorrelationIdPathConstants.EVENT_BRIDGE; +import static software.amazon.lambda.powertools.logging.CorrelationIdPaths.EVENT_BRIDGE; 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 org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.logging.Logging; public class PowertoolsLogEventBridgeCorrelationId implements RequestStreamHandler { - private final Logger LOG = LogManager.getLogger(PowertoolsLogEventBridgeCorrelationId.class); + private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogEventBridgeCorrelationId.class); @Override @Logging(correlationIdPath = EVENT_BRIDGE) diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventDisabled.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventDisabled.java similarity index 89% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventDisabled.java rename to powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventDisabled.java index 77103e450..fc1feb52d 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventDisabled.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventDisabled.java @@ -18,9 +18,9 @@ import com.amazonaws.services.lambda.runtime.RequestHandler; import software.amazon.lambda.powertools.logging.Logging; -public class PowerToolLogEventDisabled implements RequestHandler { +public class PowertoolsLogEventDisabled implements RequestHandler { - @Logging(logEvent = false) + @Logging @Override public Object handleRequest(Object input, Context context) { return null; diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabledForStream.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventForStream.java similarity index 80% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabledForStream.java rename to powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventForStream.java index 9de76586f..350b29cde 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerToolLogEventEnabledForStream.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogEventForStream.java @@ -23,12 +23,12 @@ import java.util.Map; import software.amazon.lambda.powertools.logging.Logging; -public class PowerToolLogEventEnabledForStream implements RequestStreamHandler { +public class PowertoolsLogEventForStream implements RequestStreamHandler { - @Logging(logEvent = true) @Override - public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException { + @Logging(logEvent = true) + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { ObjectMapper mapper = new ObjectMapper(); - mapper.writeValue(output, mapper.readValue(input, Map.class)); + mapper.writeValue(outputStream, mapper.readValue(inputStream, Map.class)); } } 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 new file mode 100644 index 000000000..001bde3ed --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/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.handlers; + +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/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 new file mode 100644 index 000000000..38be5c025 --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/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.handlers; + +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/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabled.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogSamplingDisabled.java similarity index 68% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabled.java rename to powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogSamplingDisabled.java index df68ea14f..5e2a7f148 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolEnabled.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogSamplingDisabled.java @@ -16,23 +16,18 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.logging.Logging; -public class PowerLogToolEnabled implements RequestHandler { - private final Logger LOG = LogManager.getLogger(PowerLogToolEnabled.class); +public class PowertoolsLogSamplingDisabled implements RequestHandler { + private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogSamplingDisabled.class); @Override - @Logging - public Object handleRequest(Object input, Context context) { + @Logging(samplingRate = 0.0) + public Boolean handleRequest(Object input, Context context) { LOG.info("Test event"); LOG.debug("Test debug event"); - return null; - } - - @Logging - public void anotherMethod() { - System.out.println("test"); + return LOG.isDebugEnabled(); } } diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolSamplingEnabled.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogSamplingEnabled.java similarity index 74% rename from powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolSamplingEnabled.java rename to powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogSamplingEnabled.java index 357520395..6a8c37896 100644 --- a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowerLogToolSamplingEnabled.java +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/handlers/PowertoolsLogSamplingEnabled.java @@ -16,18 +16,18 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.logging.Logging; -public class PowerLogToolSamplingEnabled implements RequestHandler { - private final Logger LOG = LogManager.getLogger(PowerLogToolSamplingEnabled.class); +public class PowertoolsLogSamplingEnabled implements RequestHandler { + private final Logger LOG = LoggerFactory.getLogger(PowertoolsLogSamplingEnabled.class); @Override @Logging(samplingRate = 1.0) - public Object handleRequest(Object input, Context context) { + public Boolean handleRequest(Object input, Context context) { LOG.info("Test event"); LOG.debug("Test debug event"); - return null; + return LOG.isDebugEnabled(); } } 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 453e1de29..bc5e53675 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 @@ -14,18 +14,24 @@ package software.amazon.lambda.powertools.logging.internal; -import static java.util.Collections.emptyMap; 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.fail; +import static org.assertj.core.api.Assertions.contentOf; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.openMocks; -import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; import static software.amazon.lambda.powertools.common.internal.SystemWrapper.getProperty; import static software.amazon.lambda.powertools.common.internal.SystemWrapper.getenv; +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; +import static software.amazon.lambda.powertools.logging.internal.PowertoolsLoggedFields.FUNCTION_NAME; +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.internal.PowertoolsLoggedFields.SERVICE; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; @@ -33,52 +39,61 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent; -import com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; import com.amazonaws.services.lambda.runtime.tests.annotations.Event; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; -import java.io.InputStreamReader; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; import java.lang.reflect.InvocationTargetException; 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 java.util.stream.Collectors; -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.ThreadContext; -import org.json.JSONException; +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; import org.mockito.Mock; import org.mockito.MockedStatic; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import org.slf4j.event.Level; import software.amazon.lambda.powertools.common.internal.LambdaHandlerProcessor; import software.amazon.lambda.powertools.common.internal.SystemWrapper; -import software.amazon.lambda.powertools.logging.handlers.PowerLogToolApiGatewayHttpApiCorrelationId; -import software.amazon.lambda.powertools.logging.handlers.PowerLogToolApiGatewayRestApiCorrelationId; -import software.amazon.lambda.powertools.logging.handlers.PowerLogToolEnabled; -import software.amazon.lambda.powertools.logging.handlers.PowerLogToolEnabledForStream; -import software.amazon.lambda.powertools.logging.handlers.PowerToolDisabled; -import software.amazon.lambda.powertools.logging.handlers.PowerToolDisabledForStream; -import software.amazon.lambda.powertools.logging.handlers.PowerToolLogEventDisabled; -import software.amazon.lambda.powertools.logging.handlers.PowerToolLogEventEnabled; -import software.amazon.lambda.powertools.logging.handlers.PowerToolLogEventEnabledForStream; -import software.amazon.lambda.powertools.logging.handlers.PowerToolLogEventEnabledWithCustomMapper; import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogAlbCorrelationId; -import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEnabledWithClearState; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogApiGatewayHttpApiCorrelationId; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogApiGatewayRestApiCorrelationId; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogAppSyncCorrelationId; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogClearState; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogDisabled; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogDisabledForStream; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEnabled; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogEnabledForStream; +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.PowertoolsLogEventForStream; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogResponse; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogResponseForStream; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogSamplingDisabled; +import software.amazon.lambda.powertools.logging.handlers.PowertoolsLogSamplingEnabled; class LambdaLoggingAspectTest { + private static final Logger LOG = LoggerFactory.getLogger(LambdaLoggingAspectTest.class); private static final int EXPECTED_CONTEXT_SIZE = 8; private RequestStreamHandler requestStreamHandler; private RequestHandler requestHandler; @@ -87,210 +102,327 @@ class LambdaLoggingAspectTest { private Context context; @BeforeEach - void setUp() throws IllegalAccessException, IOException, NoSuchMethodException, InvocationTargetException { + void setUp() throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, IOException { openMocks(this); - ThreadContext.clearAll(); + MDC.clear(); writeStaticField(LambdaHandlerProcessor.class, "IS_COLD_START", null, true); setupContext(); - requestHandler = new PowerLogToolEnabled(); - requestStreamHandler = new PowerLogToolEnabledForStream(); + requestHandler = new PowertoolsLogEnabled(); + requestStreamHandler = new PowertoolsLogEnabledForStream(); + resetLogLevel(Level.INFO); + writeStaticField(LoggingConstants.class, "LAMBDA_LOG_LEVEL", null, true); + writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_LEVEL", null, true); + writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_EVENT", false, true); + writeStaticField(LoggingConstants.class, "POWERTOOLS_SAMPLING_RATE", null, true); + try { + FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); + } catch (NoSuchFileException e) { + // may not be there in the first run + } + } + + @AfterEach + void cleanUp() throws IOException { //Make sure file is cleaned up before running full stack logging regression FileChannel.open(Paths.get("target/logfile.json"), StandardOpenOption.WRITE).truncate(0).close(); - resetLogLevel(Level.INFO); + } + + @Test + void shouldLogDebugWhenPowertoolsLevelEnvVarIsDebug() throws IllegalAccessException { + // GIVEN + writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_LEVEL", "DEBUG", true); + + // WHEN + LambdaLoggingAspect.setLogLevel(); + + // THEN + assertThat(LOG.isDebugEnabled()).isTrue(); + } + + @Test + void shouldLogInfoWhenPowertoolsLevelEnvVarIsInfo() throws IllegalAccessException { + // GIVEN + writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_LEVEL", "INFO", true); + + // WHEN + LambdaLoggingAspect.setLogLevel(); + + // THEN + assertThat(LOG.isDebugEnabled()).isFalse(); + assertThat(LOG.isInfoEnabled()).isTrue(); + } + + @Test + void shouldLogInfoWhenPowertoolsLevelEnvVarIsInvalid() throws IllegalAccessException { + // GIVEN + writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_LEVEL", "INVALID", true); + + // WHEN + LambdaLoggingAspect.setLogLevel(); + + // THEN + assertThat(LOG.isDebugEnabled()).isFalse(); + assertThat(LOG.isInfoEnabled()).isTrue(); + } + + @Test + void shouldLogWarnWhenPowertoolsLevelEnvVarIsWarn() throws IllegalAccessException { + // GIVEN + writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_LEVEL", "WARN", true); + + // WHEN + LambdaLoggingAspect.setLogLevel(); + + // THEN + assertThat(LOG.isDebugEnabled()).isFalse(); + assertThat(LOG.isInfoEnabled()).isFalse(); + assertThat(LOG.isWarnEnabled()).isTrue(); + } + + @Test + void shouldLogErrorWhenPowertoolsLevelEnvVarIsError() throws IllegalAccessException { + // GIVEN + writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_LEVEL", "ERROR", true); + + // WHEN + LambdaLoggingAspect.setLogLevel(); + + // THEN + assertThat(LOG.isDebugEnabled()).isFalse(); + assertThat(LOG.isInfoEnabled()).isFalse(); + assertThat(LOG.isWarnEnabled()).isFalse(); + assertThat(LOG.isErrorEnabled()).isTrue(); + } + + @Test + void shouldLogErrorWhenPowertoolsLevelEnvVarIsFatal() throws IllegalAccessException { + // GIVEN + writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_LEVEL", "FATAL", true); + + // WHEN + LambdaLoggingAspect.setLogLevel(); + + // THEN + assertThat(LOG.isDebugEnabled()).isFalse(); + assertThat(LOG.isInfoEnabled()).isFalse(); + assertThat(LOG.isWarnEnabled()).isFalse(); + assertThat(LOG.isErrorEnabled()).isTrue(); + } + + @Test + void shouldLogWarnWhenPowertoolsLevelEnvVarIsWarnAndLambdaLevelVarIsInfo() throws IllegalAccessException { + // GIVEN + writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_LEVEL", "WARN", true); + writeStaticField(LoggingConstants.class, "LAMBDA_LOG_LEVEL", "INFO", true); + + // WHEN + LambdaLoggingAspect.setLogLevel(); + + // THEN + assertThat(LOG.isDebugEnabled()).isFalse(); + assertThat(LOG.isInfoEnabled()).isFalse(); + assertThat(LOG.isWarnEnabled()).isTrue(); + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).doesNotContain(" does not match AWS Lambda Advanced Logging Controls minimum log level"); + } + + @Test + void shouldLogInfoWhenPowertoolsLevelEnvVarIsInfoAndLambdaLevelVarIsWarn() throws IllegalAccessException { + // GIVEN + writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_LEVEL", "INFO", true); + writeStaticField(LoggingConstants.class, "LAMBDA_LOG_LEVEL", "WARN", true); + + // WHEN + LambdaLoggingAspect.setLogLevel(); + + // THEN + assertThat(LOG.isDebugEnabled()).isFalse(); + assertThat(LOG.isInfoEnabled()).isTrue(); + File logFile = new File("target/logfile.json"); + // should log a warning as powertools level is lower than lambda level + assertThat(contentOf(logFile)).contains("Current log level (INFO) does not match AWS Lambda Advanced Logging Controls minimum log level (WARN). This can lead to data loss, consider adjusting them."); + } + + @Test + void shouldLogWarnWhenPowertoolsLevelEnvVarINotSetAndLambdaLevelVarIsWarn() throws IllegalAccessException { + // GIVEN + writeStaticField(LoggingConstants.class, "POWERTOOLS_LOG_LEVEL", null, true); + writeStaticField(LoggingConstants.class, "LAMBDA_LOG_LEVEL", "WARN", true); + + // WHEN + LambdaLoggingAspect.setLogLevel(); + + // THEN + assertThat(LOG.isDebugEnabled()).isFalse(); + assertThat(LOG.isInfoEnabled()).isFalse(); + assertThat(LOG.isWarnEnabled()).isTrue(); } @Test void shouldSetLambdaContextWhenEnabled() { requestHandler.handleRequest(new Object(), context); - assertThat(ThreadContext.getImmutableContext()) + assertThat(MDC.getCopyOfContextMap()) .hasSize(EXPECTED_CONTEXT_SIZE) - .containsEntry(DefaultLambdaFields.FUNCTION_ARN.getName(), "testArn") - .containsEntry(DefaultLambdaFields.FUNCTION_MEMORY_SIZE.getName(), "10") - .containsEntry(DefaultLambdaFields.FUNCTION_VERSION.getName(), "1") - .containsEntry(DefaultLambdaFields.FUNCTION_NAME.getName(), "testFunction") - .containsEntry(DefaultLambdaFields.FUNCTION_REQUEST_ID.getName(), "RequestId") - .containsKey("coldStart") - .containsKey("service"); + .containsEntry(FUNCTION_ARN.getName(), "testArn") + .containsEntry(FUNCTION_MEMORY_SIZE.getName(), "10") + .containsEntry(FUNCTION_VERSION.getName(), "1") + .containsEntry(FUNCTION_NAME.getName(), "testFunction") + .containsEntry(FUNCTION_REQUEST_ID.getName(), "RequestId") + .containsKey(FUNCTION_COLD_START.getName()) + .containsKey(SERVICE.getName()); } @Test void shouldSetLambdaContextForStreamHandlerWhenEnabled() throws IOException { - requestStreamHandler = new PowerLogToolEnabledForStream(); + requestStreamHandler = new PowertoolsLogEnabledForStream(); requestStreamHandler.handleRequest(new ByteArrayInputStream(new byte[]{}), new ByteArrayOutputStream(), context); - assertThat(ThreadContext.getImmutableContext()) + assertThat(MDC.getCopyOfContextMap()) .hasSize(EXPECTED_CONTEXT_SIZE) - .containsEntry(DefaultLambdaFields.FUNCTION_ARN.getName(), "testArn") - .containsEntry(DefaultLambdaFields.FUNCTION_MEMORY_SIZE.getName(), "10") - .containsEntry(DefaultLambdaFields.FUNCTION_VERSION.getName(), "1") - .containsEntry(DefaultLambdaFields.FUNCTION_NAME.getName(), "testFunction") - .containsEntry(DefaultLambdaFields.FUNCTION_REQUEST_ID.getName(), "RequestId") - .containsKey("coldStart") - .containsKey("service"); + .containsEntry(FUNCTION_ARN.getName(), "testArn") + .containsEntry(FUNCTION_MEMORY_SIZE.getName(), "10") + .containsEntry(FUNCTION_VERSION.getName(), "1") + .containsEntry(FUNCTION_NAME.getName(), "testFunction") + .containsEntry(FUNCTION_REQUEST_ID.getName(), "RequestId") + .containsKey(FUNCTION_COLD_START.getName()) + .containsKey(SERVICE.getName()); } @Test - void shouldSetColdStartFlag() throws IOException { + void shouldSetColdStartFlagOnFirstCallNotOnSecondCall() throws IOException { requestStreamHandler.handleRequest(new ByteArrayInputStream(new byte[]{}), new ByteArrayOutputStream(), context); - assertThat(ThreadContext.getImmutableContext()) + assertThat(MDC.getCopyOfContextMap()) .hasSize(EXPECTED_CONTEXT_SIZE) - .containsEntry("coldStart", "true"); + .containsEntry(FUNCTION_COLD_START.getName(), "true"); requestStreamHandler.handleRequest(new ByteArrayInputStream(new byte[]{}), new ByteArrayOutputStream(), context); - assertThat(ThreadContext.getImmutableContext()) + assertThat(MDC.getCopyOfContextMap()) .hasSize(EXPECTED_CONTEXT_SIZE) - .containsEntry("coldStart", "false"); + .containsEntry(FUNCTION_COLD_START.getName(), "false"); } @Test void shouldNotSetLambdaContextWhenDisabled() { - requestHandler = new PowerToolDisabled(); + requestHandler = new PowertoolsLogDisabled(); requestHandler.handleRequest(new Object(), context); - assertThat(ThreadContext.getImmutableContext()) - .isEmpty(); + assertThat(MDC.getCopyOfContextMap()).isNull(); } @Test void shouldNotSetLambdaContextForStreamHandlerWhenDisabled() throws IOException { - requestStreamHandler = new PowerToolDisabledForStream(); + requestStreamHandler = new PowertoolsLogDisabledForStream(); requestStreamHandler.handleRequest(null, null, context); - assertThat(ThreadContext.getImmutableContext()) - .isEmpty(); + assertThat(MDC.getCopyOfContextMap()).isNull(); } @Test - void shouldHaveNoEffectIfNotUsedOnLambdaHandler() { - PowerLogToolEnabled handler = new PowerLogToolEnabled(); + void shouldClearStateWhenClearStateIsTrue() { + PowertoolsLogClearState handler = new PowertoolsLogClearState(); - handler.anotherMethod(); + handler.handleRequest(Collections.singletonMap("mySuperSecret", "P@ssw0Rd"), context); - assertThat(ThreadContext.getImmutableContext()) - .isEmpty(); + assertThat(MDC.getCopyOfContextMap()).isNull(); } @Test - void shouldLogEventForHandler() throws IOException, JSONException { - requestHandler = new PowerToolLogEventEnabled(); - S3EventNotification s3EventNotification = s3EventNotification(); - - requestHandler.handleRequest(s3EventNotification, context); + void shouldLogDebugWhenSamplingEqualsOne() { + PowertoolsLogSamplingEnabled handler = new PowertoolsLogSamplingEnabled(); - Map log = parseToMap(Files.lines(Paths.get("target/logfile.json")).collect(joining())); + Boolean debugEnabled = handler.handleRequest(new Object(), context); - String event = (String) log.get("message"); - - String expectEvent = new BufferedReader( - new InputStreamReader(this.getClass().getResourceAsStream("/s3EventNotification.json"))) - .lines().collect(joining("\n")); - - assertEquals(expectEvent, event, false); + assertThat(debugEnabled).isTrue(); } - /** - * If POWERTOOLS_LOGGER_LOG_EVENT was set to true, the handler should log, despite @Logging(logEvent=false) - * - * @throws IOException - */ @Test - void shouldLogEventForHandlerWhenEnvVariableSetToTrue() throws IOException, IllegalAccessException, JSONException { - try { - writeStaticField(LambdaLoggingAspect.class, "LOG_EVENT", Boolean.TRUE, true); - - requestHandler = new PowerToolLogEventDisabled(); - S3EventNotification s3EventNotification = s3EventNotification(); - - requestHandler.handleRequest(s3EventNotification, context); + void shouldLogDebugWhenSamplingEnvVarEqualsOne() throws IllegalAccessException { + // GIVEN + LoggingConstants.POWERTOOLS_SAMPLING_RATE = "1"; + PowertoolsLogEnabled handler = new PowertoolsLogEnabled(); - Map log = parseToMap(Files.lines(Paths.get("target/logfile.json")).collect(joining())); + // WHEN + handler.handleRequest(new Object(), context); - String event = (String) log.get("message"); - - String expectEvent = new BufferedReader( - new InputStreamReader(this.getClass().getResourceAsStream("/s3EventNotification.json"))) - .lines().collect(joining("\n")); - - assertEquals(expectEvent, event, false); - } finally { - writeStaticField(LambdaLoggingAspect.class, "LOG_EVENT", Boolean.FALSE, true); - } + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).contains("Test debug event"); } - /** - * If POWERTOOLS_LOGGER_LOG_EVENT was set to false and @Logging(logEvent=false), the handler shouldn't log - * - * @throws IOException - */ @Test - void shouldNotLogEventForHandlerWhenEnvVariableSetToFalse() throws IOException { - requestHandler = new PowerToolLogEventDisabled(); - S3EventNotification s3EventNotification = s3EventNotification(); + void shouldNotLogDebugWhenSamplingEnvVarIsTooBig() throws IllegalAccessException { + // GIVEN + LoggingConstants.POWERTOOLS_SAMPLING_RATE = "42"; - requestHandler.handleRequest(s3EventNotification, context); + // WHEN + requestHandler.handleRequest(new Object(), context); - Assertions.assertEquals(0, - Files.lines(Paths.get("target/logfile.json")).collect(joining()).length()); + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).doesNotContain("Test debug event"); } @Test - void shouldLogEventForHandlerWithOverriddenObjectMapper() throws IOException, JSONException { - RequestHandler handler = new PowerToolLogEventEnabledWithCustomMapper(); - S3EventNotification s3EventNotification = s3EventNotification(); - - handler.handleRequest(s3EventNotification, context); - - Map log = parseToMap(Files.lines(Paths.get("target/logfile.json")).collect(joining())); + void shouldNotLogDebugWhenSamplingEnvVarIsInvalid() { + // GIVEN + LoggingConstants.POWERTOOLS_SAMPLING_RATE = "NotANumber"; - String event = (String) log.get("message"); - - String expectEvent = new BufferedReader( - new InputStreamReader(this.getClass().getResourceAsStream("/customizedLogEvent.json"))) - .lines().collect(joining("\n")); + // WHEN + requestHandler.handleRequest(new Object(), context); - assertEquals(expectEvent, event, false); + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).doesNotContain("Test debug event"); + assertThat(contentOf(logFile)).contains( + "Skipping sampling rate on environment variable configuration because of invalid value"); } @Test - void shouldLogEventForStreamAndLambdaStreamIsValid() throws IOException, JSONException { - requestStreamHandler = new PowerToolLogEventEnabledForStream(); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - S3EventNotification s3EventNotification = s3EventNotification(); + void shouldNotLogDebugWhenSamplingEqualsZero() { + // GIVEN + LoggingConstants.POWERTOOLS_SAMPLING_RATE = "0"; + PowertoolsLogSamplingDisabled handler = new PowertoolsLogSamplingDisabled(); - requestStreamHandler.handleRequest( - new ByteArrayInputStream(new ObjectMapper().writeValueAsBytes(s3EventNotification)), output, context); - - assertThat(new String(output.toByteArray(), StandardCharsets.UTF_8)) - .isNotEmpty(); + // WHEN + Boolean debugEnabled = handler.handleRequest(new Object(), context); - Map log = parseToMap(Files.lines(Paths.get("target/logfile.json")).collect(joining())); + // THEN + assertThat(debugEnabled).isFalse(); + } - String event = (String) log.get("message"); + @Test + void shouldHaveNoEffectIfNotUsedOnLambdaHandler() { + // GIVEN + PowertoolsLogEnabled handler = new PowertoolsLogEnabled(); - String expectEvent = new BufferedReader( - new InputStreamReader(this.getClass().getResourceAsStream("/s3EventNotification.json"))) - .lines().collect(joining("\n")); + // WHEN + handler.anotherMethod(); - assertEquals(expectEvent, event, false); + // THEN + assertThat(MDC.getCopyOfContextMap()).isNull(); } @Test void shouldLogServiceNameWhenEnvVarSet() throws IllegalAccessException { + // GIVEN writeStaticField(LambdaHandlerProcessor.class, "SERVICE_NAME", "testService", true); + + // WHEN requestHandler.handleRequest(new Object(), context); - assertThat(ThreadContext.getImmutableContext()) + // THEN + assertThat(MDC.getCopyOfContextMap()) .hasSize(EXPECTED_CONTEXT_SIZE) - .containsEntry("service", "testService"); + .containsEntry(SERVICE.getName(), "testService"); } @Test @@ -305,7 +437,7 @@ void shouldLogxRayTraceIdSystemPropertySet() { requestHandler.handleRequest(new Object(), context); - assertThat(ThreadContext.getImmutableContext()) + assertThat(MDC.getCopyOfContextMap()) .hasSize(EXPECTED_CONTEXT_SIZE + 1) .containsEntry("xray_trace_id", xRayTraceId); } @@ -313,27 +445,191 @@ void shouldLogxRayTraceIdSystemPropertySet() { @Test void shouldLogxRayTraceIdEnvVarSet() { + // GIVEN String xRayTraceId = "1-5759e988-bd862e3fe1be46a994272793"; try (MockedStatic mocked = mockStatic(SystemWrapper.class)) { mocked.when(() -> getenv("_X_AMZN_TRACE_ID")) .thenReturn("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1"); + // WHEN requestHandler.handleRequest(new Object(), context); - assertThat(ThreadContext.getImmutableContext()) + // THEN + assertThat(MDC.getCopyOfContextMap()) .hasSize(EXPECTED_CONTEXT_SIZE + 1) - .containsEntry("xray_trace_id", xRayTraceId); + .containsEntry(FUNCTION_TRACE_ID.getName(), xRayTraceId); + } + } + + @Test + void shouldLogEventForHandlerWithLogEventAnnotation() { + // GIVEN + requestHandler = new PowertoolsLogEvent(); + + // WHEN + requestHandler.handleRequest(singletonList("ListOfOneElement"), context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).contains("[\"ListOfOneElement\"]"); + } + + @Test + void shouldLogEventForHandlerWhenEnvVariableSetToTrue() throws IllegalAccessException { + try { + // GIVEN + LoggingConstants.POWERTOOLS_LOG_EVENT = true; + + 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("\"body\":\"body\"").contains("\"messageId\":\"1234abcd\"").contains("\"awsRegion\":\"eu-west-1\""); + } finally { + LoggingConstants.POWERTOOLS_LOG_EVENT = false; + } + } + + @Test + void shouldNotLogEventForHandlerWhenEnvVariableSetToFalse() throws IOException { + // GIVEN + LoggingConstants.POWERTOOLS_LOG_EVENT = false; + + // WHEN + requestHandler = new PowertoolsLogEventDisabled(); + requestHandler.handleRequest(singletonList("ListOfOneElement"), context); + + // THEN + Assertions.assertEquals(0, + Files.lines(Paths.get("target/logfile.json")).collect(joining()).length()); + } + + @Test + void shouldLogEventForStreamHandler() throws IOException { + // GIVEN + 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("{\"key\":\"value\"}"); + } + + @Test + void shouldLogResponseForHandlerWithLogResponseAnnotation() { + // GIVEN + requestHandler = new PowertoolsLogResponse(); + + // WHEN + requestHandler.handleRequest("input", context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).contains("Hola mundo"); + } + + @Test + void shouldLogResponseForHandlerWhenEnvVariableSetToTrue() throws IllegalAccessException { + try { + // GIVEN + LoggingConstants.POWERTOOLS_LOG_RESPONSE = true; + + requestHandler = new PowertoolsLogEnabled(); + + // WHEN + requestHandler.handleRequest("input", context); + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).contains("Bonjour le monde"); + } finally { + LoggingConstants.POWERTOOLS_LOG_RESPONSE = false; + } + } + + @Test + void shouldLogResponseForStreamHandler() throws IOException { + // GIVEN + 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(input); + } + + @Test + void shouldLogErrorForHandlerWithLogErrorAnnotation() { + // GIVEN + requestHandler = new PowertoolsLogError(); + + // WHEN + try { + requestHandler.handleRequest("input", context); + } catch (Exception e) { + // ignore + } + + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).contains("This is an error"); + } + + @Test + void shouldLogErrorForHandlerWhenEnvVariableSetToTrue() throws IllegalAccessException { + try { + // GIVEN + LoggingConstants.POWERTOOLS_LOG_ERROR = true; + + requestHandler = new PowertoolsLogEnabled(true); + + // WHEN + try { + requestHandler.handleRequest("input", context); + } catch (Exception e) { + // ignore + } + // THEN + File logFile = new File("target/logfile.json"); + assertThat(contentOf(logFile)).contains("Something went wrong"); + } finally { + LoggingConstants.POWERTOOLS_LOG_ERROR = false; } } @ParameterizedTest @Event(value = "apiGatewayProxyEventV1.json", type = APIGatewayProxyRequestEvent.class) void shouldLogCorrelationIdOnAPIGatewayProxyRequestEvent(APIGatewayProxyRequestEvent event) { - RequestHandler handler = new PowerLogToolApiGatewayRestApiCorrelationId(); + // GIVEN + RequestHandler handler = new PowertoolsLogApiGatewayRestApiCorrelationId(); + + // WHEN handler.handleRequest(event, context); - assertThat(ThreadContext.getImmutableContext()) + // THEN + assertThat(MDC.getCopyOfContextMap()) .hasSize(EXPECTED_CONTEXT_SIZE + 1) .containsEntry("correlation_id", event.getRequestContext().getRequestId()); } @@ -341,10 +637,14 @@ void shouldLogCorrelationIdOnAPIGatewayProxyRequestEvent(APIGatewayProxyRequestE @ParameterizedTest @Event(value = "apiGatewayProxyEventV2.json", type = APIGatewayV2HTTPEvent.class) void shouldLogCorrelationIdOnAPIGatewayV2HTTPEvent(APIGatewayV2HTTPEvent event) { - RequestHandler handler = new PowerLogToolApiGatewayHttpApiCorrelationId(); + // GIVEN + RequestHandler handler = new PowertoolsLogApiGatewayHttpApiCorrelationId(); + + // WHEN handler.handleRequest(event, context); - assertThat(ThreadContext.getImmutableContext()) + // THEN + assertThat(MDC.getCopyOfContextMap()) .hasSize(EXPECTED_CONTEXT_SIZE + 1) .containsEntry("correlation_id", event.getRequestContext().getRequestId()); } @@ -352,46 +652,92 @@ void shouldLogCorrelationIdOnAPIGatewayV2HTTPEvent(APIGatewayV2HTTPEvent event) @ParameterizedTest @Event(value = "albEvent.json", type = ApplicationLoadBalancerRequestEvent.class) void shouldLogCorrelationIdOnALBEvent(ApplicationLoadBalancerRequestEvent event) { + // GIVEN RequestHandler handler = new PowertoolsLogAlbCorrelationId(); + + // WHEN handler.handleRequest(event, context); - assertThat(ThreadContext.getImmutableContext()) + // THEN + assertThat(MDC.getCopyOfContextMap()) .hasSize(EXPECTED_CONTEXT_SIZE + 1) .containsEntry("correlation_id", event.getHeaders().get("x-amzn-trace-id")); } @Test void shouldLogCorrelationIdOnStreamHandler() throws IOException { + // GIVEN RequestStreamHandler handler = new PowertoolsLogEventBridgeCorrelationId(); String eventId = "3"; - String event = "{\"id\":" + eventId + "}"; // CorrelationIdPathConstants.EVENT_BRIDGE + String event = "{\"id\":" + eventId + "}"; // CorrelationIdPath.EVENT_BRIDGE ByteArrayInputStream inputStream = new ByteArrayInputStream(event.getBytes()); + + // WHEN handler.handleRequest(inputStream, new ByteArrayOutputStream(), context); + // THEN + assertThat(MDC.getCopyOfContextMap()) + .hasSize(EXPECTED_CONTEXT_SIZE + 1) + .containsEntry("correlation_id", eventId); + } - assertThat(ThreadContext.getImmutableContext()) + @Test + void shouldLogCorrelationIdOnAppSyncEvent() throws IOException { + // GIVEN + RequestStreamHandler handler = new PowertoolsLogAppSyncCorrelationId(); + String eventId = "456"; + String event = "{\"request\":{\"headers\":{\"x-amzn-trace-id\":" + eventId + "}}}"; // CorrelationIdPath.APPSYNC_RESOLVER + ByteArrayInputStream inputStream = new ByteArrayInputStream(event.getBytes()); + + // WHEN + handler.handleRequest(inputStream, new ByteArrayOutputStream(), context); + + // THEN + assertThat(MDC.getCopyOfContextMap()) .hasSize(EXPECTED_CONTEXT_SIZE + 1) .containsEntry("correlation_id", eventId); } @Test - void shouldLogAndClearLogContextOnEachRequest() throws IOException { - requestHandler = new PowertoolsLogEnabledWithClearState(); - S3EventNotification s3EventNotification = s3EventNotification(); + void testMultipleLoggingManagers_shouldWarnAndSelectFirstOne() throws UnsupportedEncodingException { + // GIVEN + List list = new ArrayList<>(); + list.add(new TestLoggingManager()); + list.add(new DefautlLoggingManager()); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PrintStream stream = new PrintStream(outputStream); + + // WHEN + LambdaLoggingAspect.getLoggingManager(list, stream); + + // THEN + String output = outputStream.toString("UTF-8"); + assertThat(output) + .contains("WARN. Multiple LoggingManagers were found on the classpath") + .contains("WARN. Make sure to have only one of powertools-logging-log4j OR powertools-logging-logback to your dependencies") + .contains("WARN. Using the first LoggingManager found on the classpath: [" + list.get(0) + "]"); + } - requestHandler.handleRequest(s3EventNotification, context); - requestHandler.handleRequest(s3EventNotification, context); + @Test + void testNoLoggingManagers_shouldWarnAndCreateDefault() throws UnsupportedEncodingException { + // GIVEN + List list = new ArrayList<>(); - List logLines = Files.lines(Paths.get("target/logfile.json")).collect(Collectors.toList()); - Map invokeLog = parseToMap(logLines.get(0)); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PrintStream stream = new PrintStream(outputStream); - assertThat(invokeLog) - .containsEntry("TestKey", "TestValue"); + // WHEN + LoggingManager loggingManager = LambdaLoggingAspect.getLoggingManager(list, stream); - invokeLog = parseToMap(logLines.get(1)); + // THEN + String output = outputStream.toString("UTF-8"); + assertThat(output) + .contains("ERROR. No LoggingManager was found on the classpath") + .contains("ERROR. Applying default LoggingManager: POWERTOOLS_LOG_LEVEL variable is ignored") + .contains("ERROR. Make sure to add either powertools-logging-log4j or powertools-logging-logback to your dependencies"); - assertThat(invokeLog) - .doesNotContainKey("TestKey"); + assertThat(loggingManager).isExactlyInstanceOf(DefautlLoggingManager.class); } private void setupContext() { @@ -404,44 +750,9 @@ private void setupContext() { private void resetLogLevel(Level level) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { - Method resetLogLevels = LambdaLoggingAspect.class.getDeclaredMethod("resetLogLevels", Level.class); - resetLogLevels.setAccessible(true); - resetLogLevels.invoke(null, level); + Method setLogLevels = LambdaLoggingAspect.class.getDeclaredMethod("setLogLevels", Level.class); + setLogLevels.setAccessible(true); + setLogLevels.invoke(null, level); writeStaticField(LambdaLoggingAspect.class, "LEVEL_AT_INITIALISATION", level, true); } - - private S3EventNotification s3EventNotification() { - S3EventNotification.S3EventNotificationRecord record = - new S3EventNotification.S3EventNotificationRecord("us-west-2", - "ObjectCreated:Put", - "aws:s3", - null, - "2.1", - new S3EventNotification.RequestParametersEntity("127.0.0.1"), - new S3EventNotification.ResponseElementsEntity("C3D13FE58DE4C810", - "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD"), - new S3EventNotification.S3Entity("testConfigRule", - new S3EventNotification.S3BucketEntity("mybucket", - new S3EventNotification.UserIdentityEntity("A3NL1KOZZKExample"), - "arn:aws:s3:::mybucket"), - new S3EventNotification.S3ObjectEntity("HappyFace.jpg", - 1024L, - "d41d8cd98f00b204e9800998ecf8427e", - "096fKKXTRTtl3on89fVO.nfljtsv6qko", - "0055AED6DCD90281E5"), - "1.0"), - new S3EventNotification.UserIdentityEntity("AIDAJDPLRKLG7UEXAMPLE") - ); - - return new S3EventNotification(singletonList(record)); - } - - private Map parseToMap(String stringAsJson) { - try { - return new ObjectMapper().readValue(stringAsJson, Map.class); - } catch (JsonProcessingException e) { - fail("Failed parsing logger line " + stringAsJson); - return emptyMap(); - } - } } \ No newline at end of file diff --git a/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/TestLoggingManager.java b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/TestLoggingManager.java new file mode 100644 index 000000000..0958e0d3b --- /dev/null +++ b/powertools-logging/src/test/java/software/amazon/lambda/powertools/logging/internal/TestLoggingManager.java @@ -0,0 +1,32 @@ +package software.amazon.lambda.powertools.logging.internal; + +import org.slf4j.ILoggerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; +import org.slf4j.test.TestLogger; +import org.slf4j.test.TestLoggerFactory; + +public class TestLoggingManager implements LoggingManager { + + private final TestLoggerFactory loggerFactory; + + public TestLoggingManager() { + ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory(); + if (!(loggerFactory instanceof TestLoggerFactory)) { + throw new RuntimeException( + "LoggerFactory does not match required type: " + TestLoggerFactory.class.getName()); + } + this.loggerFactory = (TestLoggerFactory) loggerFactory; + } + + @Override + public void setLogLevel(Level logLevel) { + loggerFactory.getLoggers().forEach((key, logger) -> ((TestLogger) logger).setLogLevel(logLevel.toString())); + } + + @Override + public Level getLogLevel(Logger logger) { + return org.slf4j.event.Level.intToLevel(((TestLogger) logger).getLogLevel()); + } +} diff --git a/powertools-logging/src/test/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider b/powertools-logging/src/test/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider new file mode 100644 index 000000000..ade4bb1e2 --- /dev/null +++ b/powertools-logging/src/test/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider @@ -0,0 +1 @@ +org.slf4j.test.TestServiceProvider \ No newline at end of file diff --git a/powertools-logging/src/test/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager b/powertools-logging/src/test/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager new file mode 100644 index 000000000..adbf7ae69 --- /dev/null +++ b/powertools-logging/src/test/resources/META-INF/services/software.amazon.lambda.powertools.logging.internal.LoggingManager @@ -0,0 +1,15 @@ +# +# 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. +# +# + +software.amazon.lambda.powertools.logging.internal.TestLoggingManager \ No newline at end of file diff --git a/powertools-logging/src/test/resources/log4j2.xml b/powertools-logging/src/test/resources/log4j2.xml deleted file mode 100644 index 22a44ee8b..000000000 --- a/powertools-logging/src/test/resources/log4j2.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/powertools-logging/src/test/resources/s3EventNotification.json b/powertools-logging/src/test/resources/s3EventNotification.json deleted file mode 100644 index feb88ec02..000000000 --- a/powertools-logging/src/test/resources/s3EventNotification.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "records":[ - { - "eventVersion":"2.1", - "eventSource":"aws:s3", - "awsRegion":"us-west-2", - "eventName":"ObjectCreated:Put", - "userIdentity":{ - "principalId":"AIDAJDPLRKLG7UEXAMPLE" - }, - "requestParameters":{ - "sourceIPAddress":"127.0.0.1" - }, - "responseElements":{ - "xAmzId2":"C3D13FE58DE4C810", - "xAmzRequestId":"FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD" - }, - "s3":{ - "s3SchemaVersion":"1.0", - "configurationId":"testConfigRule", - "bucket":{ - "name":"mybucket", - "ownerIdentity":{ - "principalId":"A3NL1KOZZKExample" - }, - "arn":"arn:aws:s3:::mybucket" - }, - "object":{ - "key":"HappyFace.jpg", - "size":1024, - "eTag":"d41d8cd98f00b204e9800998ecf8427e", - "versionId":"096fKKXTRTtl3on89fVO.nfljtsv6qko", - "sequencer":"0055AED6DCD90281E5" - } - } - } - ] -} \ No newline at end of file diff --git a/powertools-logging/src/test/resources/testlogger.properties b/powertools-logging/src/test/resources/testlogger.properties new file mode 100644 index 000000000..84b7beaae --- /dev/null +++ b/powertools-logging/src/test/resources/testlogger.properties @@ -0,0 +1,3 @@ +org.slf4j.simpleLogger.defaultLogLevel=warn +org.slf4j.simpleLogger.log.software.amazon.lambda.powertools=info +org.slf4j.simpleLogger.logFile=target/logfile.json \ No newline at end of file diff --git a/powertools-metrics/pom.xml b/powertools-metrics/pom.xml index 8c9ce79ad..7de1efa3f 100644 --- a/powertools-metrics/pom.xml +++ b/powertools-metrics/pom.xml @@ -27,7 +27,7 @@ 2.0.0-SNAPSHOT - Powertools for AWS Lambda (Java) library Metrics + Powertools for AWS Lambda (Java) - Metrics A suite of utilities for AWS Lambda Functions that make creating custom metrics via AWS Embedded Metric Format asynchronously easier. @@ -56,6 +56,11 @@ + + org.aspectj + aspectjrt + provided + software.amazon.lambda powertools-common diff --git a/powertools-parameters/pom.xml b/powertools-parameters/pom.xml index 1bc662457..54065aab2 100644 --- a/powertools-parameters/pom.xml +++ b/powertools-parameters/pom.xml @@ -26,11 +26,16 @@ powertools-parameters - Powertools for AWS Lambda (Java) library Parameters + Powertools for AWS Lambda (Java) - Parameters Set of utilities to retrieve parameters - common interface + + org.aspectj + aspectjrt + provided + software.amazon.lambda powertools-common diff --git a/powertools-serialization/pom.xml b/powertools-serialization/pom.xml index d92d68fb0..d1b2de826 100644 --- a/powertools-serialization/pom.xml +++ b/powertools-serialization/pom.xml @@ -27,7 +27,7 @@ powertools-serialization jar - Powertools for AWS Lambda (Java) library Serialization Utilities + Powertools for AWS Lambda (Java) - Serialization Utilities @@ -64,8 +64,8 @@ aws-lambda-java-events - org.apache.logging.log4j - log4j-slf4j2-impl + org.slf4j + slf4j-api com.fasterxml.jackson.core 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 baa6a0367..e961f21fa 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 @@ -82,6 +82,6 @@ public void addFunction(T function) { } private static class ConfigHolder { - private final static JsonConfig instance = new JsonConfig(); + private static final JsonConfig instance = new JsonConfig(); } } diff --git a/powertools-tracing/pom.xml b/powertools-tracing/pom.xml index 6498d3772..1625fd0cb 100644 --- a/powertools-tracing/pom.xml +++ b/powertools-tracing/pom.xml @@ -27,7 +27,7 @@ 2.0.0-SNAPSHOT - Powertools for AWS Lambda (Java) library Tracing + Powertools for AWS Lambda (Java) - Tracing A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, structured logging and creating custom metrics asynchronously easier. @@ -56,6 +56,11 @@ + + org.aspectj + aspectjrt + provided + software.amazon.lambda powertools-common diff --git a/powertools-validation/pom.xml b/powertools-validation/pom.xml index 269513a46..bd57fa6c5 100644 --- a/powertools-validation/pom.xml +++ b/powertools-validation/pom.xml @@ -27,7 +27,7 @@ 2.0.0-SNAPSHOT - Powertools for AWS Lambda (Java) validation library + Powertools for AWS Lambda (Java) - Validation Json schema validation for Lambda events and responses diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationConfig.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationConfig.java index 143f0584d..ccc5a4c2c 100644 --- a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationConfig.java +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/ValidationConfig.java @@ -97,6 +97,6 @@ public ObjectMapper getObjectMapper() { } private static class ConfigHolder { - private final static ValidationConfig instance = new ValidationConfig(); + private static final ValidationConfig instance = new ValidationConfig(); } } diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index dc22f22b6..2d61cc68d 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -46,22 +46,6 @@ - - - - - - - - - - - - - - - - @@ -80,20 +64,12 @@ - - - - - - + + - - - - - - + + @@ -130,12 +106,21 @@ + + + + + + + + + - + @@ -156,7 +141,7 @@ - +