From d9aa7c12ab36b700d829ca7b2475d601300dd566 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 5 Aug 2025 13:38:10 +0200 Subject: [PATCH 01/14] chore(ci): Make E2E asset deployment compatible with updated CDK lib version. Refactor retry4j to resilience4j. --- powertools-e2e-tests/pom.xml | 19 +---- .../powertools/testutils/Infrastructure.java | 17 ++++- .../powertools/testutils/RetryUtils.java | 73 +++++++++++++++++++ .../metrics/MetricDataNotFoundException.java | 25 +++++++ .../testutils/metrics/MetricsFetcher.java | 31 ++------ .../testutils/tracing/TraceFetcher.java | 65 ++++++----------- .../tracing/TraceNotFoundException.java | 25 +++++++ 7 files changed, 168 insertions(+), 87 deletions(-) create mode 100644 powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/RetryUtils.java create mode 100644 powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricDataNotFoundException.java create mode 100644 powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceNotFoundException.java diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index 1b36d4701..b5d1ee192 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -40,14 +40,12 @@ log4j-slf4j2-impl test - software.amazon.awssdk lambda ${aws.sdk.version} test - software.amazon.awssdk dynamodb @@ -66,72 +64,61 @@ ${aws.sdk.version} test - software.amazon.awssdk xray ${aws.sdk.version} test - software.amazon.awssdk sqs ${aws.sdk.version} test - com.amazonaws amazon-sqs-java-extended-client-lib 2.1.2 test - software.amazon.awssdk url-connection-client test - org.junit.jupiter junit-jupiter-api test - commons-io commons-io 2.20.0 - org.junit.jupiter junit-jupiter-engine test - org.assertj assertj-core test - - com.evanlennick - retry4j - 0.15.0 + io.github.resilience4j + resilience4j-retry + 2.3.0 test - software.amazon.awscdk aws-cdk-lib ${cdk.version} test - software.constructs constructs diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java index c65871a0a..aa969a1b5 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.nio.file.Paths; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import java.util.Objects; import java.util.UUID; @@ -469,10 +470,18 @@ private Map findAssets() { files.iterator().forEachRemaining(file -> { String assetPath = file.get("source").get("path").asText(); String assetPackaging = file.get("source").get("packaging").asText(); - String bucketName = file.get("destinations").get("current_account-current_region").get("bucketName") - .asText(); - String objectKey = file.get("destinations").get("current_account-current_region").get("objectKey") - .asText(); + JsonNode destinations = file.get("destinations"); + String bucketName = null; + String objectKey = null; + Iterator fieldNames = destinations.fieldNames(); + while (fieldNames.hasNext()) { + String fieldName = fieldNames.next(); + if (fieldName.startsWith("current_account-current_region")) { + bucketName = destinations.get(fieldName).get("bucketName").asText(); + objectKey = destinations.get(fieldName).get("objectKey").asText(); + break; + } + } Asset asset = new Asset(assetPath, assetPackaging, bucketName.replace("${AWS::AccountId}", account) .replace("${AWS::Region}", region.toString())); assets.put(objectKey, asset); diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/RetryUtils.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/RetryUtils.java new file mode 100644 index 000000000..6f641db74 --- /dev/null +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/RetryUtils.java @@ -0,0 +1,73 @@ +/* + * 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.testutils; + +import java.time.Duration; +import java.util.function.Supplier; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.github.resilience4j.retry.Retry; +import io.github.resilience4j.retry.RetryConfig; + +/** + * Utility class for consistent retry configuration across all test utilities. + */ +public class RetryUtils { + private static final Logger LOG = LoggerFactory.getLogger(RetryUtils.class); + + private static final RetryConfig DEFAULT_RETRY_CONFIG = RetryConfig.custom() + .maxAttempts(60) // 60 attempts over 5 minutes + .waitDuration(Duration.ofSeconds(5)) // 5 seconds between attempts + .build(); + + private RetryUtils() { + // Utility class + } + + /** + * Creates a retry instance with default configuration for the specified exception type. + * + * @param name the name for the retry instance + * @param retryOnException the exception class to retry on + * @return configured Retry instance + */ + public static Retry createRetry(String name, Class retryOnException) { + RetryConfig config = RetryConfig.from(DEFAULT_RETRY_CONFIG) + .retryExceptions(retryOnException) + .build(); + + Retry retry = Retry.of(name, config); + retry.getEventPublisher().onRetry(event -> LOG.warn("Retry attempt {} for {}: {}", + event.getNumberOfRetryAttempts(), name, event.getLastThrowable().getMessage())); + + return retry; + } + + /** + * Decorates a supplier with retry logic for the specified exception type. + * + * @param supplier the supplier to decorate + * @param name the name for the retry instance + * @param retryOnException the exception class to retry on + * @return decorated supplier with retry logic + */ + public static Supplier withRetry(Supplier supplier, String name, + Class retryOnException) { + Retry retry = createRetry(name, retryOnException); + return Retry.decorateSupplier(retry, supplier); + } +} diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricDataNotFoundException.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricDataNotFoundException.java new file mode 100644 index 000000000..3b0508c84 --- /dev/null +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricDataNotFoundException.java @@ -0,0 +1,25 @@ +/* + * 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.testutils.metrics; + +/** + * Exception thrown when metric data is not found in CloudWatch. + * This exception is used to trigger retries as metrics may not be available immediately. + */ +public class MetricDataNotFoundException extends RuntimeException { + public MetricDataNotFoundException(String message) { + super(message); + } +} diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricsFetcher.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricsFetcher.java index 186e72d13..b0768b43c 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricsFetcher.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricsFetcher.java @@ -14,20 +14,15 @@ package software.amazon.lambda.powertools.testutils.metrics; -import static java.time.Duration.ofSeconds; - -import com.evanlennick.retry4j.CallExecutor; -import com.evanlennick.retry4j.CallExecutorBuilder; -import com.evanlennick.retry4j.Status; -import com.evanlennick.retry4j.config.RetryConfig; -import com.evanlennick.retry4j.config.RetryConfigBuilder; import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.concurrent.Callable; +import java.util.function.Supplier; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; import software.amazon.awssdk.regions.Region; @@ -39,6 +34,7 @@ import software.amazon.awssdk.services.cloudwatch.model.MetricDataQuery; import software.amazon.awssdk.services.cloudwatch.model.MetricStat; import software.amazon.awssdk.services.cloudwatch.model.StandardUnit; +import software.amazon.lambda.powertools.testutils.RetryUtils; /** * Class in charge of retrieving the actual metrics of a Lambda execution on CloudWatch @@ -73,7 +69,7 @@ public List fetchMetrics(Instant start, Instant end, int period, String dimensions.forEach((key, value) -> dimensionsList.add(Dimension.builder().name(key).value(value).build())); } - Callable> callable = () -> { + Supplier> supplier = () -> { LOG.debug("Get Metrics for namespace {}, start {}, end {}, metric {}, dimensions {}", namespace, start, end, metricName, dimensionsList); GetMetricDataResponse metricData = cloudwatch.getMetricData(GetMetricDataRequest.builder() @@ -96,24 +92,11 @@ public List fetchMetrics(Instant start, Instant end, int period, String .build()); List values = metricData.metricDataResults().get(0).values(); if (values == null || values.isEmpty()) { - throw new Exception("No data found for metric " + metricName); + throw new MetricDataNotFoundException("No data found for metric " + metricName); } return values; }; - RetryConfig retryConfig = new RetryConfigBuilder() - .withMaxNumberOfTries(10) - .retryOnAnyException() - .withDelayBetweenTries(ofSeconds(2)) - .withRandomExponentialBackoff() - .build(); - CallExecutor> callExecutor = new CallExecutorBuilder>() - .config(retryConfig) - .afterFailedTryListener(s -> { - LOG.warn("{}, attempts: {}", s.getLastExceptionThatCausedRetry().getMessage(), s.getTotalTries()); - }) - .build(); - Status> status = callExecutor.execute(callable); - return status.getResult(); + return RetryUtils.withRetry(supplier, "metrics-fetcher-" + metricName, MetricDataNotFoundException.class).get(); } } diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceFetcher.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceFetcher.java index 2b5b6e15b..8da848da0 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceFetcher.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceFetcher.java @@ -14,25 +14,21 @@ package software.amazon.lambda.powertools.testutils.tracing; -import static java.time.Duration.ofSeconds; - -import com.evanlennick.retry4j.CallExecutor; -import com.evanlennick.retry4j.CallExecutorBuilder; -import com.evanlennick.retry4j.Status; -import com.evanlennick.retry4j.config.RetryConfig; -import com.evanlennick.retry4j.config.RetryConfigBuilder; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.concurrent.Callable; +import java.util.function.Supplier; import java.util.stream.Collectors; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; import software.amazon.awssdk.regions.Region; @@ -43,6 +39,7 @@ import software.amazon.awssdk.services.xray.model.GetTraceSummariesResponse; import software.amazon.awssdk.services.xray.model.TimeRangeType; import software.amazon.awssdk.services.xray.model.TraceSummary; +import software.amazon.lambda.powertools.testutils.RetryUtils; import software.amazon.lambda.powertools.testutils.tracing.SegmentDocument.SubSegment; /** @@ -50,8 +47,8 @@ */ public class TraceFetcher { - private static final ObjectMapper MAPPER = - new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + private static final ObjectMapper MAPPER = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); private static final Logger LOG = LoggerFactory.getLogger(TraceFetcher.class); private static final SdkHttpClient httpClient = UrlConnectionHttpClient.builder().build(); private static final Region region = Region.of(System.getProperty("AWS_DEFAULT_REGION", "eu-west-1")); @@ -88,27 +85,12 @@ public static Builder builder() { * @return traces */ public Trace fetchTrace() { - Callable callable = () -> - { + Supplier supplier = () -> { List traceIds = getTraceIds(); return getTrace(traceIds); }; - RetryConfig retryConfig = new RetryConfigBuilder() - .withMaxNumberOfTries(10) - .retryOnAnyException() - .withDelayBetweenTries(ofSeconds(5)) - .withRandomExponentialBackoff() - .build(); - CallExecutor callExecutor = new CallExecutorBuilder() - .config(retryConfig) - .afterFailedTryListener(s -> - { - LOG.warn(s.getLastExceptionThatCausedRetry().getMessage() + ", attempts: " + s.getTotalTries()); - }) - .build(); - Status status = callExecutor.execute(callable); - return status.getResult(); + return RetryUtils.withRetry(supplier, "trace-fetcher", TraceNotFoundException.class).get(); } /** @@ -122,14 +104,12 @@ private Trace getTrace(List traceIds) { .traceIds(traceIds) .build()); if (!tracesResponse.hasTraces()) { - throw new RuntimeException("No trace found"); + throw new TraceNotFoundException("No trace found"); } Trace traceRes = new Trace(); - tracesResponse.traces().forEach(trace -> - { + tracesResponse.traces().forEach(trace -> { if (trace.hasSegments()) { - trace.segments().forEach(segment -> - { + trace.segments().forEach(segment -> { try { SegmentDocument document = MAPPER.readValue(segment.document(), SegmentDocument.class); if (document.getOrigin().equals("AWS::Lambda::Function")) { @@ -149,8 +129,7 @@ private Trace getTrace(List traceIds) { } private void getNestedSubSegments(List subsegments, Trace traceRes, List idsToIgnore) { - subsegments.forEach(subsegment -> - { + subsegments.forEach(subsegment -> { List subSegmentIdsToIgnore = Collections.emptyList(); if (!excludedSegments.contains(subsegment.getName()) && !idsToIgnore.contains(subsegment.getId())) { traceRes.addSubSegment(subsegment); @@ -179,12 +158,12 @@ private List getTraceIds() { .filterExpression(filterExpression) .build()); if (!traceSummaries.hasTraceSummaries()) { - throw new RuntimeException("No trace id found"); + throw new TraceNotFoundException("No trace id found"); } - List traceIds = - traceSummaries.traceSummaries().stream().map(TraceSummary::id).collect(Collectors.toList()); + List traceIds = traceSummaries.traceSummaries().stream().map(TraceSummary::id) + .collect(Collectors.toList()); if (traceIds.isEmpty()) { - throw new RuntimeException("No trace id found"); + throw new TraceNotFoundException("No trace id found"); } return traceIds; } @@ -236,8 +215,8 @@ public Builder excludeSegments(List excludedSegments) { } public Builder functionName(String functionName) { - this.filterExpression = - String.format("service(id(name: \"%s\", type: \"AWS::Lambda::Function\"))", functionName); + this.filterExpression = String.format("service(id(name: \"%s\", type: \"AWS::Lambda::Function\"))", + functionName); return this; } } diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceNotFoundException.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceNotFoundException.java new file mode 100644 index 000000000..692280adf --- /dev/null +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceNotFoundException.java @@ -0,0 +1,25 @@ +/* + * 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.testutils.tracing; + +/** + * Exception thrown when trace data is not found in X-Ray. + * This exception is used to trigger retries as traces may not be available immediately. + */ +public class TraceNotFoundException extends RuntimeException { + public TraceNotFoundException(String message) { + super(message); + } +} From 4e846329b511d52a6afca3cc75a841d2a9cb7ce4 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 5 Aug 2025 15:27:47 +0200 Subject: [PATCH 02/14] Add Retry logic to tests instead of Thread.sleep where appropriate. --- .../lambda/powertools/e2e/Function.java | 2 +- .../amazon/lambda/powertools/BatchE2ET.java | 95 ++++++++----- .../lambda/powertools/IdempotencyE2ET.java | 11 +- .../lambda/powertools/LargeMessageE2ET.java | 55 +++++--- .../LargeMessageIdempotentE2ET.java | 41 ++++-- .../amazon/lambda/powertools/TracingE2ET.java | 22 +-- .../lambda/powertools/ValidationALBE2ET.java | 127 ++++++++--------- .../powertools/ValidationApiGWE2ET.java | 132 +++++++++--------- .../testutils/DataNotReadyException.java | 25 ++++ .../powertools/testutils/RetryUtils.java | 18 +-- .../metrics/MetricDataNotFoundException.java | 4 +- .../tracing/TraceNotFoundException.java | 4 +- 12 files changed, 321 insertions(+), 215 deletions(-) create mode 100644 powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/DataNotReadyException.java diff --git a/powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Function.java index 16109778d..038704931 100644 --- a/powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Function.java +++ b/powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -60,4 +60,4 @@ public String handleRequest(Input input, Context context) { DateTimeFormatter dtf = DateTimeFormatter.ISO_DATE_TIME.withZone(TimeZone.getTimeZone("UTC").toZoneId()); return dtf.format(Instant.now()); } -} \ No newline at end of file +} diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java index c5f74594d..7278134f9 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java @@ -15,10 +15,7 @@ package software.amazon.lambda.powertools; import static org.assertj.core.api.Assertions.assertThat; -import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -26,11 +23,16 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; + import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; @@ -44,18 +46,18 @@ import software.amazon.awssdk.services.kinesis.KinesisClient; import software.amazon.awssdk.services.kinesis.model.PutRecordsRequest; import software.amazon.awssdk.services.kinesis.model.PutRecordsRequestEntry; -import software.amazon.awssdk.services.kinesis.model.PutRecordsResponse; import software.amazon.awssdk.services.sqs.SqsClient; import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequest; import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequestEntry; +import software.amazon.lambda.powertools.testutils.DataNotReadyException; import software.amazon.lambda.powertools.testutils.Infrastructure; +import software.amazon.lambda.powertools.testutils.RetryUtils; import software.amazon.lambda.powertools.utilities.JsonConfig; -public class BatchE2ET { +class BatchE2ET { private static final SdkHttpClient httpClient = UrlConnectionHttpClient.builder().build(); private static final Region region = Region.of(System.getProperty("AWS_DEFAULT_REGION", "eu-west-1")); private static Infrastructure infrastructure; - private static String functionName; private static String queueUrl; private static String kinesisStreamName; @@ -71,13 +73,12 @@ public BatchE2ET() { testProducts = Arrays.asList( new Product(1, "product1", 1.23), new Product(2, "product2", 4.56), - new Product(3, "product3", 6.78) - ); + new Product(3, "product3", 6.78)); } @BeforeAll @Timeout(value = 5, unit = TimeUnit.MINUTES) - public static void setup() { + static void setup() { String random = UUID.randomUUID().toString().substring(0, 6); String queueName = "batchqueue" + random; kinesisStreamName = "batchstream" + random; @@ -94,7 +95,6 @@ public static void setup() { .build(); Map outputs = infrastructure.deploy(); - functionName = outputs.get(FUNCTION_NAME_OUTPUT); queueUrl = outputs.get("QueueURL"); kinesisStreamName = outputs.get("KinesisStreamName"); outputTable = outputs.get("TableNameForAsyncTests"); @@ -117,14 +117,14 @@ public static void setup() { } @AfterAll - public static void tearDown() { + static void tearDown() { if (infrastructure != null) { infrastructure.destroy(); } } @AfterEach - public void cleanUpTest() { + void cleanUpTest() { // Delete everything in the output table ScanResponse items = ddbClient.scan(ScanRequest.builder() .tableName(outputTable) @@ -150,7 +150,7 @@ public void cleanUpTest() { } @Test - public void sqsBatchProcessingSucceeds() throws InterruptedException { + void sqsBatchProcessingSucceeds() { List entries = testProducts.stream() .map(p -> { try { @@ -169,17 +169,25 @@ public void sqsBatchProcessingSucceeds() throws InterruptedException { .entries(entries) .queueUrl(queueUrl) .build()); - Thread.sleep(30000); // wait for function to be executed // THEN - ScanResponse items = ddbClient.scan(ScanRequest.builder() - .tableName(outputTable) - .build()); - validateAllItemsHandled(items); + // When the retry loop finishes it means we found all products in the result + RetryUtils.withRetry(() -> { + ScanResponse items = ddbClient.scan(ScanRequest.builder() + .tableName(outputTable) + .build()); + if (!areAllTestProductsPresent(items)) { + throw new DataNotReadyException("sqs-batch-processing not complete yet"); + } + return null; + }, "sqs-batch-processing", DataNotReadyException.class).get(); + + ScanResponse finalItems = ddbClient.scan(ScanRequest.builder().tableName(outputTable).build()); + assertThat(areAllTestProductsPresent(finalItems)).isTrue(); } @Test - public void kinesisBatchProcessingSucceeds() throws InterruptedException { + void kinesisBatchProcessingSucceeds() { List entries = testProducts.stream() .map(p -> { try { @@ -194,21 +202,29 @@ public void kinesisBatchProcessingSucceeds() throws InterruptedException { .collect(Collectors.toList()); // WHEN - PutRecordsResponse result = kinesisClient.putRecords(PutRecordsRequest.builder() + kinesisClient.putRecords(PutRecordsRequest.builder() .streamName(kinesisStreamName) .records(entries) .build()); - Thread.sleep(30000); // wait for function to be executed // THEN - ScanResponse items = ddbClient.scan(ScanRequest.builder() - .tableName(outputTable) - .build()); - validateAllItemsHandled(items); + // When the retry loop finishes it means we found all products in the result + RetryUtils.withRetry(() -> { + ScanResponse items = ddbClient.scan(ScanRequest.builder() + .tableName(outputTable) + .build()); + if (!areAllTestProductsPresent(items)) { + throw new DataNotReadyException("kinesis-batch-processing not complete yet"); + } + return null; + }, "kinesis-batch-processing", DataNotReadyException.class).get(); + + ScanResponse finalItems = ddbClient.scan(ScanRequest.builder().tableName(outputTable).build()); + assertThat(areAllTestProductsPresent(finalItems)).isTrue(); } @Test - public void ddbStreamsBatchProcessingSucceeds() throws InterruptedException { + void ddbStreamsBatchProcessingSucceeds() { // GIVEN String theId = "my-test-id"; @@ -223,27 +239,38 @@ public void ddbStreamsBatchProcessingSucceeds() throws InterruptedException { } }) .build()); - Thread.sleep(90000); // wait for function to be executed // THEN - ScanResponse items = ddbClient.scan(ScanRequest.builder() - .tableName(outputTable) - .build()); + RetryUtils.withRetry(() -> { + ScanResponse items = ddbClient.scan(ScanRequest.builder() + .tableName(outputTable) + .build()); - assertThat(items.count()).isEqualTo(1); - assertThat(items.items().get(0).get("id").s()).isEqualTo(theId); + if (items.count() != 1) { + throw new DataNotReadyException("DDB streams processing not complete yet"); + } + return null; + }, "ddb-streams-batch-processing", DataNotReadyException.class).get(); + + ScanResponse finalItems = ddbClient.scan(ScanRequest.builder().tableName(outputTable).build()); + assertThat(finalItems.count()).isEqualTo(1); + assertThat(finalItems.items().get(0).get("id").s()).isEqualTo(theId); } - private void validateAllItemsHandled(ScanResponse items) { + private boolean areAllTestProductsPresent(ScanResponse items) { for (Product p : testProducts) { boolean foundIt = false; for (Map a : items.items()) { if (a.get("id").s().equals(Long.toString(p.id))) { foundIt = true; + break; } } - assertThat(foundIt).isTrue(); + if (!foundIt) { + return false; + } } + return true; } class Product { diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/IdempotencyE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/IdempotencyE2ET.java index 242d1a2db..292f46bfa 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/IdempotencyE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/IdempotencyE2ET.java @@ -21,21 +21,23 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; + import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; + import software.amazon.lambda.powertools.testutils.Infrastructure; import software.amazon.lambda.powertools.testutils.lambda.InvocationResult; -public class IdempotencyE2ET { +class IdempotencyE2ET { private static Infrastructure infrastructure; private static String functionName; @BeforeAll @Timeout(value = 5, unit = TimeUnit.MINUTES) - public static void setup() { + static void setup() { String random = UUID.randomUUID().toString().substring(0, 6); infrastructure = Infrastructure.builder() .testName(IdempotencyE2ET.class.getSimpleName()) @@ -47,14 +49,14 @@ public static void setup() { } @AfterAll - public static void tearDown() { + static void tearDown() { if (infrastructure != null) { infrastructure.destroy(); } } @Test - public void test_ttlNotExpired_sameResult_ttlExpired_differentResult() throws InterruptedException { + void test_ttlNotExpired_sameResult_ttlExpired_differentResult() throws InterruptedException { // GIVEN String event = "{\"message\":\"TTL 10sec\"}"; @@ -65,6 +67,7 @@ public void test_ttlNotExpired_sameResult_ttlExpired_differentResult() throws In // Second invocation (should get same result) InvocationResult result2 = invokeFunction(functionName, event); + // Function idempotency record expiration is set to 10 seconds Thread.sleep(12000); // Third invocation (should get different result) diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java index d9c3ef749..fabe557fa 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java @@ -36,9 +36,11 @@ import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.sqs.SqsClient; import software.amazon.awssdk.services.sqs.model.SendMessageRequest; +import software.amazon.lambda.powertools.testutils.DataNotReadyException; import software.amazon.lambda.powertools.testutils.Infrastructure; +import software.amazon.lambda.powertools.testutils.RetryUtils; -public class LargeMessageE2ET { +class LargeMessageE2ET { private static final Logger LOG = LoggerFactory.getLogger(LargeMessageE2ET.class); private static final SdkHttpClient httpClient = UrlConnectionHttpClient.builder().build(); @@ -62,7 +64,7 @@ public class LargeMessageE2ET { @BeforeAll @Timeout(value = 5, unit = TimeUnit.MINUTES) - public static void setup() { + static void setup() { String random = UUID.randomUUID().toString().substring(0, 6); bucketName = "largemessagebucket" + random; String queueName = "largemessagequeue" + random; @@ -84,14 +86,14 @@ public static void setup() { } @AfterAll - public static void tearDown() { + static void tearDown() { if (infrastructure != null) { infrastructure.destroy(); } } @AfterEach - public void reset() { + void reset() { if (messageId != null) { Map itemToDelete = new HashMap<>(); itemToDelete.put("functionName", AttributeValue.builder().s(functionName).build()); @@ -102,8 +104,8 @@ public void reset() { } @Test - public void bigSQSMessageOffloadedToS3_shouldLoadFromS3() throws IOException, InterruptedException { - // given + void bigSQSMessageOffloadedToS3_shouldLoadFromS3() throws IOException { + // GIVEN final ExtendedClientConfiguration extendedClientConfig = new ExtendedClientConfiguration() .withPayloadSupportEnabled(s3Client, bucketName); try (AmazonSQSExtendedClient client = new AmazonSQSExtendedClient( @@ -111,16 +113,15 @@ public void bigSQSMessageOffloadedToS3_shouldLoadFromS3() throws IOException, In InputStream inputStream = this.getClass().getResourceAsStream("/large_sqs_message.txt"); String bigMessage = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - // when + // WHEN client.sendMessage(SendMessageRequest .builder() .queueUrl(queueUrl) .messageBody(bigMessage) .build()); } - Thread.sleep(30000); // wait for function to be executed - // then + // THEN QueryRequest request = QueryRequest .builder() .tableName(tableName) @@ -128,8 +129,17 @@ public void bigSQSMessageOffloadedToS3_shouldLoadFromS3() throws IOException, In .expressionAttributeValues( Collections.singletonMap(":func", AttributeValue.builder().s(functionName).build())) .build(); - QueryResponse response = dynamoDbClient.query(request); - List> items = response.items(); + + RetryUtils.withRetry(() -> { + QueryResponse response = dynamoDbClient.query(request); + if (response.items().size() != 1) { + throw new DataNotReadyException("Large message processing not complete yet"); + } + return null; + }, "large-message-processing", DataNotReadyException.class).get(); + + QueryResponse finalResponse = dynamoDbClient.query(request); + List> items = finalResponse.items(); assertThat(items).hasSize(1); messageId = items.get(0).get("id").s(); assertThat(Integer.valueOf(items.get(0).get("bodySize").n())).isEqualTo(300977); @@ -137,8 +147,8 @@ public void bigSQSMessageOffloadedToS3_shouldLoadFromS3() throws IOException, In } @Test - public void smallSQSMessage_shouldNotReadFromS3() throws IOException, InterruptedException { - // given + void smallSQSMessage_shouldNotReadFromS3() throws IOException { + // GIVEN final ExtendedClientConfiguration extendedClientConfig = new ExtendedClientConfiguration() .withPayloadSupportEnabled(s3Client, bucketName); try (AmazonSQSExtendedClient client = new AmazonSQSExtendedClient( @@ -146,16 +156,14 @@ public void smallSQSMessage_shouldNotReadFromS3() throws IOException, Interrupte extendedClientConfig)) { String message = "Hello World"; - // when + // WHEN client.sendMessage(SendMessageRequest .builder() .queueUrl(queueUrl) .messageBody(message) .build()); - Thread.sleep(30000); // wait for function to be executed - - // then + // THEN QueryRequest request = QueryRequest .builder() .tableName(tableName) @@ -163,8 +171,17 @@ public void smallSQSMessage_shouldNotReadFromS3() throws IOException, Interrupte .expressionAttributeValues( Collections.singletonMap(":func", AttributeValue.builder().s(functionName).build())) .build(); - QueryResponse response = dynamoDbClient.query(request); - List> items = response.items(); + + RetryUtils.withRetry(() -> { + QueryResponse response = dynamoDbClient.query(request); + if (response.items().size() != 1) { + throw new DataNotReadyException("Small message processing not complete yet"); + } + return null; + }, "small-message-processing", DataNotReadyException.class).get(); + + QueryResponse finalResponse = dynamoDbClient.query(request); + List> items = finalResponse.items(); assertThat(items).hasSize(1); messageId = items.get(0).get("id").s(); assertThat(Integer.valueOf(items.get(0).get("bodySize").n())).isEqualTo( diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageIdempotentE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageIdempotentE2ET.java index ef342ea13..f1d56f0d0 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageIdempotentE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageIdempotentE2ET.java @@ -24,6 +24,7 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; + import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -31,6 +32,7 @@ import org.junit.jupiter.api.Timeout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; @@ -44,9 +46,11 @@ import software.amazon.awssdk.services.sqs.SqsClient; import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; import software.amazon.awssdk.services.sqs.model.SendMessageRequest; +import software.amazon.lambda.powertools.testutils.DataNotReadyException; import software.amazon.lambda.powertools.testutils.Infrastructure; +import software.amazon.lambda.powertools.testutils.RetryUtils; -public class LargeMessageIdempotentE2ET { +class LargeMessageIdempotentE2ET { private static final Logger LOG = LoggerFactory.getLogger(LargeMessageIdempotentE2ET.class); private static final SdkHttpClient httpClient = UrlConnectionHttpClient.builder().build(); @@ -72,10 +76,9 @@ public class LargeMessageIdempotentE2ET { private static String queueUrl; private static String tableName; - @BeforeAll @Timeout(value = 5, unit = TimeUnit.MINUTES) - public static void setup() { + static void setup() { String random = UUID.randomUUID().toString().substring(0, 6); bucketName = "largemessagebucket" + random; String queueName = "largemessagequeue" + random; @@ -98,14 +101,14 @@ public static void setup() { } @AfterAll - public static void tearDown() { + static void tearDown() { if (infrastructure != null) { infrastructure.destroy(); } } @Test - public void test_ttlNotExpired_doesNotInsertInDDB_ttlExpired_insertInDDB() throws InterruptedException, + void test_ttlNotExpired_doesNotInsertInDDB_ttlExpired_insertInDDB() throws InterruptedException, IOException { int waitMs = 15000; @@ -135,7 +138,6 @@ public void test_ttlNotExpired_doesNotInsertInDDB_ttlExpired_insertInDDB() throw // First invocation // send message to SQS with the good pointer and metadata sqsClient.sendMessage(messageRequest); - Thread.sleep(waitMs); // wait for the function to be invoked & executed // THEN QueryRequest request = QueryRequest @@ -145,6 +147,15 @@ public void test_ttlNotExpired_doesNotInsertInDDB_ttlExpired_insertInDDB() throw .expressionAttributeValues( Collections.singletonMap(":func", AttributeValue.builder().s(functionName).build())) .build(); + + RetryUtils.withRetry(() -> { + QueryResponse response = dynamoDbClient.query(request); + if (response.items().size() != 1) { + throw new DataNotReadyException("First invocation processing not complete yet"); + } + return null; + }, "first-invocation-processing", DataNotReadyException.class).get(); + QueryResponse response = dynamoDbClient.query(request); List> items = response.items(); assertThat(items).hasSize(1); @@ -156,7 +167,14 @@ public void test_ttlNotExpired_doesNotInsertInDDB_ttlExpired_insertInDDB() throw // Second invocation // send the same message before ttl expires sqsClient.sendMessage(messageRequest); - Thread.sleep(waitMs); // wait for the function to be invoked & executed + + RetryUtils.withRetry(() -> { + QueryResponse resp = dynamoDbClient.query(request); + if (resp.items().size() != 1) { + throw new DataNotReadyException("Second invocation processing not complete yet"); + } + return null; + }, "second-invocation-processing", DataNotReadyException.class).get(); // THEN response = dynamoDbClient.query(request); @@ -174,7 +192,14 @@ public void test_ttlNotExpired_doesNotInsertInDDB_ttlExpired_insertInDDB() throw // Third invocation // send the same message again sqsClient.sendMessage(messageRequest); - Thread.sleep(waitMs); // wait for the function to be invoked + + RetryUtils.withRetry(() -> { + QueryResponse resp = dynamoDbClient.query(request); + if (resp.items().size() != 2) { + throw new DataNotReadyException("Third invocation processing not complete yet"); + } + return null; + }, "third-invocation-processing", DataNotReadyException.class).get(); // THEN response = dynamoDbClient.query(request); diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java index d2a5ceed1..cdf7f7de3 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java @@ -15,6 +15,7 @@ package software.amazon.lambda.powertools; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT; import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; @@ -33,21 +34,21 @@ import software.amazon.lambda.powertools.testutils.tracing.Trace; import software.amazon.lambda.powertools.testutils.tracing.TraceFetcher; -public class TracingE2ET { - private static final String service = "TracingE2EService_" + UUID.randomUUID(); +class TracingE2ET { + private static final String SERVICE = "TracingE2EService_" + UUID.randomUUID(); private static Infrastructure infrastructure; private static String functionName; @BeforeAll - @Timeout(value = 5, unit = TimeUnit.MINUTES) - public static void setup() { + @Timeout(value = 10, unit = TimeUnit.MINUTES) + static void setup() { infrastructure = Infrastructure.builder() .testName(TracingE2ET.class.getSimpleName()) .pathToFunction("tracing") .tracing(true) .environmentVariables( - Map.of("POWERTOOLS_SERVICE_NAME", service, + Map.of("POWERTOOLS_SERVICE_NAME", SERVICE, "POWERTOOLS_TRACER_CAPTURE_RESPONSE", "true")) .build(); Map outputs = infrastructure.deploy(); @@ -55,14 +56,14 @@ public static void setup() { } @AfterAll - public static void tearDown() { + static void tearDown() { if (infrastructure != null) { infrastructure.destroy(); } } @Test - public void test_tracing() { + void test_tracing() { // GIVEN final String message = "Hello World"; final String event = String.format("{\"message\":\"%s\"}", message); @@ -92,12 +93,13 @@ public void test_tracing() { final SubSegment handleRequestSegment = trace.getSubsegments().stream() .filter(subSegment -> subSegment.getName().equals("## handleRequest")) .findFirst().orElse(null); + assertNotNull(handleRequestSegment); assertThat(handleRequestSegment.getName()).isEqualTo("## handleRequest"); assertThat(handleRequestSegment.getAnnotations()).hasSize(2); assertThat(handleRequestSegment.getAnnotations()).containsEntry("ColdStart", true); - assertThat(handleRequestSegment.getAnnotations()).containsEntry("Service", service); + assertThat(handleRequestSegment.getAnnotations()).containsEntry("Service", SERVICE); assertThat(handleRequestSegment.getMetadata()).hasSize(1); - final Map metadata = (Map) handleRequestSegment.getMetadata().get(service); + final Map metadata = (Map) handleRequestSegment.getMetadata().get(SERVICE); assertThat(metadata).containsEntry("handleRequest response", result); assertThat(handleRequestSegment.getSubsegments()).hasSize(2); @@ -115,7 +117,7 @@ public void test_tracing() { assertThat(buildMessage.getAnnotations()).containsEntry("message", message); assertThat(buildMessage.getMetadata()).hasSize(1); final Map buildMessageSegmentMetadata = (Map) buildMessage.getMetadata() - .get(service); + .get(SERVICE); assertThat(buildMessageSegmentMetadata).containsEntry("buildMessage response", result); } } diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java index 41696943a..330c3130e 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java @@ -38,67 +38,68 @@ class ValidationALBE2ET { - private static final ObjectMapper objectMapper = new ObjectMapper(); - - private static Infrastructure infrastructure; - private static String functionName; - - @BeforeAll - @Timeout(value = 5, unit = TimeUnit.MINUTES) - public static void setup() { - infrastructure = Infrastructure.builder().testName(ValidationALBE2ET.class.getSimpleName()) - .pathToFunction("validation-alb-event").build(); - Map outputs = infrastructure.deploy(); - functionName = outputs.get(FUNCTION_NAME_OUTPUT); - } - - @AfterAll - public static void tearDown() { - if (infrastructure != null) { - infrastructure.destroy(); - } - } - - @Test - void test_validInboundSQSEvent() throws IOException { - InputStream inputStream = this.getClass().getResourceAsStream("/validation/valid_alb_in_out_event.json"); - String validEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - - // WHEN - InvocationResult invocationResult = invokeFunction(functionName, validEvent); - - // THEN - // invocation should pass validation and return 200 - JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); - assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(200); - assertThat(validJsonNode.get("body").asText()).isEqualTo("{\"price\": 150}"); - } - - @Test - void test_invalidInboundSQSEvent() throws IOException { - InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_alb_in_event.json"); - String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - - // WHEN - InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); - - // THEN - // invocation should fail inbound validation and return an error message - JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); - assertThat(validJsonNode.get("errorMessage").asText()).contains(": required property 'price' not found"); - } - - @Test - void test_invalidOutboundSQSEvent() throws IOException { - InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_alb_out_event.json"); - String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - - // WHEN - InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); - - // THEN - // invocation should fail outbound validation and return 400 - JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); - assertThat(validJsonNode.get("errorMessage").asText()).contains("/price: must have an exclusive maximum value of 1000"); - } + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static Infrastructure infrastructure; + private static String functionName; + + @BeforeAll + @Timeout(value = 5, unit = TimeUnit.MINUTES) + static void setup() { + infrastructure = Infrastructure.builder().testName(ValidationALBE2ET.class.getSimpleName()) + .pathToFunction("validation-alb-event").build(); + Map outputs = infrastructure.deploy(); + functionName = outputs.get(FUNCTION_NAME_OUTPUT); + } + + @AfterAll + static void tearDown() { + if (infrastructure != null) { + infrastructure.destroy(); + } + } + + @Test + void test_validInboundSQSEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/valid_alb_in_out_event.json"); + String validEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, validEvent); + + // THEN + // invocation should pass validation and return 200 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(200); + assertThat(validJsonNode.get("body").asText()).isEqualTo("{\"price\": 150}"); + } + + @Test + void test_invalidInboundSQSEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_alb_in_event.json"); + String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); + + // THEN + // invocation should fail inbound validation and return an error message + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("errorMessage").asText()).contains(": required property 'price' not found"); + } + + @Test + void test_invalidOutboundSQSEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_alb_out_event.json"); + String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); + + // THEN + // invocation should fail outbound validation and return 400 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("errorMessage").asText()) + .contains("/price: must have an exclusive maximum value of 1000"); + } } diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java index 425399c95..e813d2965 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java @@ -38,70 +38,70 @@ class ValidationApiGWE2ET { - private static final ObjectMapper objectMapper = new ObjectMapper(); - - private static Infrastructure infrastructure; - private static String functionName; - - @BeforeAll - @Timeout(value = 5, unit = TimeUnit.MINUTES) - public static void setup() { - infrastructure = Infrastructure.builder().testName(ValidationApiGWE2ET.class.getSimpleName()) - .pathToFunction("validation-apigw-event").build(); - Map outputs = infrastructure.deploy(); - functionName = outputs.get(FUNCTION_NAME_OUTPUT); - } - - @AfterAll - public static void tearDown() { - if (infrastructure != null) { - infrastructure.destroy(); - } - } - - @Test - void test_validInboundApiGWEvent() throws IOException { - InputStream inputStream = this.getClass().getResourceAsStream("/validation/valid_api_gw_in_out_event.json"); - String validEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - - // WHEN - InvocationResult invocationResult = invokeFunction(functionName, validEvent); - - // THEN - // invocation should pass validation and return 200 - JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); - assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(200); - assertThat(validJsonNode.get("body").asText()).isEqualTo("{\"price\": 150}"); - } - - @Test - void test_invalidInboundApiGWEvent() throws IOException { - InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_api_gw_in_event.json"); - String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - - // WHEN - InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); - - // THEN - // invocation should fail inbound validation and return 400 - JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); - assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400); - assertThat(validJsonNode.get("body").asText()).contains(": required property 'price' not found"); - } - - @Test - void test_invalidOutboundApiGWEvent() throws IOException { - InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_api_gw_out_event.json"); - String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - - // WHEN - InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); - - // THEN - // invocation should fail outbound validation and return 400 - JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); - assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400); - assertThat(validJsonNode.get("body").asText()) - .contains("/price: must have an exclusive maximum value of 1000"); - } + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static Infrastructure infrastructure; + private static String functionName; + + @BeforeAll + @Timeout(value = 5, unit = TimeUnit.MINUTES) + static void setup() { + infrastructure = Infrastructure.builder().testName(ValidationApiGWE2ET.class.getSimpleName()) + .pathToFunction("validation-apigw-event").build(); + Map outputs = infrastructure.deploy(); + functionName = outputs.get(FUNCTION_NAME_OUTPUT); + } + + @AfterAll + static void tearDown() { + if (infrastructure != null) { + infrastructure.destroy(); + } + } + + @Test + void test_validInboundApiGWEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/valid_api_gw_in_out_event.json"); + String validEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, validEvent); + + // THEN + // invocation should pass validation and return 200 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(200); + assertThat(validJsonNode.get("body").asText()).isEqualTo("{\"price\": 150}"); + } + + @Test + void test_invalidInboundApiGWEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_api_gw_in_event.json"); + String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); + + // THEN + // invocation should fail inbound validation and return 400 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400); + assertThat(validJsonNode.get("body").asText()).contains(": required property 'price' not found"); + } + + @Test + void test_invalidOutboundApiGWEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_api_gw_out_event.json"); + String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); + + // THEN + // invocation should fail outbound validation and return 400 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400); + assertThat(validJsonNode.get("body").asText()) + .contains("/price: must have an exclusive maximum value of 1000"); + } } diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/DataNotReadyException.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/DataNotReadyException.java new file mode 100644 index 000000000..0c16bb68b --- /dev/null +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/DataNotReadyException.java @@ -0,0 +1,25 @@ +/* + * 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.testutils; + +/** + * Exception thrown when test data is not ready yet. + * This exception is used to trigger retries in tests waiting for async operations. + */ +public class DataNotReadyException extends RuntimeException { + public DataNotReadyException(String message) { + super(message); + } +} diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/RetryUtils.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/RetryUtils.java index 6f641db74..bab597036 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/RetryUtils.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/RetryUtils.java @@ -39,15 +39,16 @@ private RetryUtils() { } /** - * Creates a retry instance with default configuration for the specified exception type. + * Creates a retry instance with default configuration for the specified throwable types. * * @param name the name for the retry instance - * @param retryOnException the exception class to retry on + * @param retryOnThrowables the throwable classes to retry on * @return configured Retry instance */ - public static Retry createRetry(String name, Class retryOnException) { + @SafeVarargs + public static Retry createRetry(String name, Class... retryOnThrowables) { RetryConfig config = RetryConfig.from(DEFAULT_RETRY_CONFIG) - .retryExceptions(retryOnException) + .retryExceptions(retryOnThrowables) .build(); Retry retry = Retry.of(name, config); @@ -58,16 +59,17 @@ public static Retry createRetry(String name, Class retryOnE } /** - * Decorates a supplier with retry logic for the specified exception type. + * Decorates a supplier with retry logic for the specified throwable types. * * @param supplier the supplier to decorate * @param name the name for the retry instance - * @param retryOnException the exception class to retry on + * @param retryOnThrowables the throwable classes to retry on * @return decorated supplier with retry logic */ + @SafeVarargs public static Supplier withRetry(Supplier supplier, String name, - Class retryOnException) { - Retry retry = createRetry(name, retryOnException); + Class... retryOnThrowables) { + Retry retry = createRetry(name, retryOnThrowables); return Retry.decorateSupplier(retry, supplier); } } diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricDataNotFoundException.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricDataNotFoundException.java index 3b0508c84..79478c14e 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricDataNotFoundException.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricDataNotFoundException.java @@ -14,11 +14,13 @@ package software.amazon.lambda.powertools.testutils.metrics; +import software.amazon.lambda.powertools.testutils.DataNotReadyException; + /** * Exception thrown when metric data is not found in CloudWatch. * This exception is used to trigger retries as metrics may not be available immediately. */ -public class MetricDataNotFoundException extends RuntimeException { +public class MetricDataNotFoundException extends DataNotReadyException { public MetricDataNotFoundException(String message) { super(message); } diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceNotFoundException.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceNotFoundException.java index 692280adf..453aae669 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceNotFoundException.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceNotFoundException.java @@ -14,11 +14,13 @@ package software.amazon.lambda.powertools.testutils.tracing; +import software.amazon.lambda.powertools.testutils.DataNotReadyException; + /** * Exception thrown when trace data is not found in X-Ray. * This exception is used to trigger retries as traces may not be available immediately. */ -public class TraceNotFoundException extends RuntimeException { +public class TraceNotFoundException extends DataNotReadyException { public TraceNotFoundException(String message) { super(message); } From caa75c00001fb7f6c0a9913cacba35f59e96527c Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 5 Aug 2025 15:30:53 +0200 Subject: [PATCH 03/14] Avoid request building code duplication in BatchE2ET.java. --- .../amazon/lambda/powertools/BatchE2ET.java | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java index 7278134f9..177a8b787 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java @@ -171,18 +171,16 @@ void sqsBatchProcessingSucceeds() { .build()); // THEN - // When the retry loop finishes it means we found all products in the result + ScanRequest scanRequest = ScanRequest.builder().tableName(outputTable).build(); RetryUtils.withRetry(() -> { - ScanResponse items = ddbClient.scan(ScanRequest.builder() - .tableName(outputTable) - .build()); + ScanResponse items = ddbClient.scan(scanRequest); if (!areAllTestProductsPresent(items)) { throw new DataNotReadyException("sqs-batch-processing not complete yet"); } return null; }, "sqs-batch-processing", DataNotReadyException.class).get(); - ScanResponse finalItems = ddbClient.scan(ScanRequest.builder().tableName(outputTable).build()); + ScanResponse finalItems = ddbClient.scan(scanRequest); assertThat(areAllTestProductsPresent(finalItems)).isTrue(); } @@ -208,18 +206,16 @@ void kinesisBatchProcessingSucceeds() { .build()); // THEN - // When the retry loop finishes it means we found all products in the result + ScanRequest scanRequest = ScanRequest.builder().tableName(outputTable).build(); RetryUtils.withRetry(() -> { - ScanResponse items = ddbClient.scan(ScanRequest.builder() - .tableName(outputTable) - .build()); + ScanResponse items = ddbClient.scan(scanRequest); if (!areAllTestProductsPresent(items)) { throw new DataNotReadyException("kinesis-batch-processing not complete yet"); } return null; }, "kinesis-batch-processing", DataNotReadyException.class).get(); - ScanResponse finalItems = ddbClient.scan(ScanRequest.builder().tableName(outputTable).build()); + ScanResponse finalItems = ddbClient.scan(scanRequest); assertThat(areAllTestProductsPresent(finalItems)).isTrue(); } @@ -241,18 +237,16 @@ void ddbStreamsBatchProcessingSucceeds() { .build()); // THEN + ScanRequest scanRequest = ScanRequest.builder().tableName(outputTable).build(); RetryUtils.withRetry(() -> { - ScanResponse items = ddbClient.scan(ScanRequest.builder() - .tableName(outputTable) - .build()); - + ScanResponse items = ddbClient.scan(scanRequest); if (items.count() != 1) { throw new DataNotReadyException("DDB streams processing not complete yet"); } return null; }, "ddb-streams-batch-processing", DataNotReadyException.class).get(); - ScanResponse finalItems = ddbClient.scan(ScanRequest.builder().tableName(outputTable).build()); + ScanResponse finalItems = ddbClient.scan(scanRequest); assertThat(finalItems.count()).isEqualTo(1); assertThat(finalItems.items().get(0).get("id").s()).isEqualTo(theId); } From 1628b6ceb25bd88accd91d56133a71670b5d89b8 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 5 Aug 2025 15:31:47 +0200 Subject: [PATCH 04/14] Include TracingE2ET again after updating retry logic. --- powertools-e2e-tests/pom.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index b5d1ee192..b2a203c33 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -213,9 +213,6 @@ **/*E2ET.java - - **/TracingE2ET.java - @@ -245,9 +242,6 @@ **/LoggingE2ET.java **/ParametersE2ET.java - - **/TracingE2ET.java - true From def7df7e902aade4ce4be1bf45e8459f672ebfef Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 5 Aug 2025 15:42:25 +0200 Subject: [PATCH 05/14] Address Sonarlint issues. --- .../software/amazon/lambda/powertools/LargeMessageE2ET.java | 2 +- .../amazon/lambda/powertools/LargeMessageIdempotentE2ET.java | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java index fabe557fa..d4454e06f 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java @@ -147,7 +147,7 @@ void bigSQSMessageOffloadedToS3_shouldLoadFromS3() throws IOException { } @Test - void smallSQSMessage_shouldNotReadFromS3() throws IOException { + void smallSQSMessage_shouldNotReadFromS3() { // GIVEN final ExtendedClientConfiguration extendedClientConfig = new ExtendedClientConfiguration() .withPayloadSupportEnabled(s3Client, bucketName); diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageIdempotentE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageIdempotentE2ET.java index f1d56f0d0..55c963451 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageIdempotentE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageIdempotentE2ET.java @@ -110,8 +110,6 @@ static void tearDown() { @Test void test_ttlNotExpired_doesNotInsertInDDB_ttlExpired_insertInDDB() throws InterruptedException, IOException { - int waitMs = 15000; - // GIVEN InputStream inputStream = this.getClass().getResourceAsStream("/large_sqs_message.txt"); String bigMessage = IOUtils.toString(inputStream, StandardCharsets.UTF_8); From 55447b54b72f19c45ed86a4cd727698e193e3fa8 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 5 Aug 2025 15:49:49 +0200 Subject: [PATCH 06/14] Downgrade resilience4j to Java11 compatible version 1.x --- powertools-e2e-tests/pom.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index b2a203c33..52abc2b45 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -110,7 +110,8 @@ io.github.resilience4j resilience4j-retry - 2.3.0 + + 1.7.1 test From 420e16183c1d87f51f5db4f5643a045746910d95 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 5 Aug 2025 16:07:19 +0200 Subject: [PATCH 07/14] Fix pmd_analyze issues. --- .../amazon/lambda/powertools/BatchE2ET.java | 2 +- .../lambda/powertools/LargeMessageE2ET.java | 4 ++-- .../LargeMessageIdempotentE2ET.java | 21 +++++++++--------- .../amazon/lambda/powertools/TracingE2ET.java | 2 +- .../lambda/powertools/ValidationALBE2ET.java | 22 +++++++++---------- 5 files changed, 26 insertions(+), 25 deletions(-) diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java index 177a8b787..9adf67695 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java @@ -131,7 +131,7 @@ void cleanUpTest() { .build()); for (Map item : items.items()) { - HashMap key = new HashMap() { + Map key = new HashMap() { { put("functionName", AttributeValue.builder() .s(item.get("functionName").s()) diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java index d4454e06f..74247ca2e 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java @@ -109,8 +109,8 @@ void bigSQSMessageOffloadedToS3_shouldLoadFromS3() throws IOException { final ExtendedClientConfiguration extendedClientConfig = new ExtendedClientConfiguration() .withPayloadSupportEnabled(s3Client, bucketName); try (AmazonSQSExtendedClient client = new AmazonSQSExtendedClient( - SqsClient.builder().region(region).httpClient(httpClient).build(), extendedClientConfig)) { - InputStream inputStream = this.getClass().getResourceAsStream("/large_sqs_message.txt"); + SqsClient.builder().region(region).httpClient(httpClient).build(), extendedClientConfig); + InputStream inputStream = this.getClass().getResourceAsStream("/large_sqs_message.txt");) { String bigMessage = IOUtils.toString(inputStream, StandardCharsets.UTF_8); // WHEN diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageIdempotentE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageIdempotentE2ET.java index 55c963451..cd787b60a 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageIdempotentE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageIdempotentE2ET.java @@ -111,22 +111,23 @@ static void tearDown() { void test_ttlNotExpired_doesNotInsertInDDB_ttlExpired_insertInDDB() throws InterruptedException, IOException { // GIVEN - InputStream inputStream = this.getClass().getResourceAsStream("/large_sqs_message.txt"); - String bigMessage = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - - // upload manually to S3 - String key = UUID.randomUUID().toString(); - s3Client.putObject(PutObjectRequest.builder() - .bucket(bucketName) - .key(key) - .build(), RequestBody.fromString(bigMessage)); + String s3Key = UUID.randomUUID().toString(); + try (InputStream inputStream = this.getClass().getResourceAsStream("/large_sqs_message.txt")) { + String bigMessage = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // upload manually to S3 + s3Client.putObject(PutObjectRequest.builder() + .bucket(bucketName) + .key(s3Key) + .build(), RequestBody.fromString(bigMessage)); + } // WHEN SendMessageRequest messageRequest = SendMessageRequest.builder() .queueUrl(queueUrl) .messageBody(String.format( "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"%s\",\"s3Key\":\"%s\"}]", - bucketName, key)) + bucketName, s3Key)) .messageAttributes(Collections.singletonMap("SQSLargePayloadSize", MessageAttributeValue.builder() .stringValue("300977") .dataType("Number") diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java index cdf7f7de3..853899777 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java @@ -91,7 +91,7 @@ void test_tracing() { assertThat(initSegment.getAnnotations()).isNull(); final SubSegment handleRequestSegment = trace.getSubsegments().stream() - .filter(subSegment -> subSegment.getName().equals("## handleRequest")) + .filter(subSegment -> "## handleRequest".equals(subSegment.getName())) .findFirst().orElse(null); assertNotNull(handleRequestSegment); assertThat(handleRequestSegment.getName()).isEqualTo("## handleRequest"); diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java index 330c3130e..7042e1ccc 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java @@ -61,17 +61,17 @@ static void tearDown() { @Test void test_validInboundSQSEvent() throws IOException { - InputStream inputStream = this.getClass().getResourceAsStream("/validation/valid_alb_in_out_event.json"); - String validEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - - // WHEN - InvocationResult invocationResult = invokeFunction(functionName, validEvent); - - // THEN - // invocation should pass validation and return 200 - JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); - assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(200); - assertThat(validJsonNode.get("body").asText()).isEqualTo("{\"price\": 150}"); + try (InputStream is = this.getClass().getResourceAsStream("/validation/valid_alb_in_out_event.json")) { + String validEvent = IOUtils.toString(is, StandardCharsets.UTF_8); + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, validEvent); + + // THEN + // invocation should pass validation and return 200 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(200); + assertThat(validJsonNode.get("body").asText()).isEqualTo("{\"price\": 150}"); + } } @Test From 7d269015fa5e4c0e0b0189cffc1780d190ee2b86 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 5 Aug 2025 16:12:00 +0200 Subject: [PATCH 08/14] Fix more pmd_analyze issues. --- .../amazon/lambda/powertools/BatchE2ET.java | 2 +- .../lambda/powertools/ValidationALBE2ET.java | 36 ++++++----- .../powertools/ValidationApiGWE2ET.java | 63 ++++++++++--------- 3 files changed, 53 insertions(+), 48 deletions(-) diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java index 9adf67695..28d5f9c24 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java @@ -131,7 +131,7 @@ void cleanUpTest() { .build()); for (Map item : items.items()) { - Map key = new HashMap() { + Map key = new HashMap<>() { { put("functionName", AttributeValue.builder() .s(item.get("functionName").s()) diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java index 7042e1ccc..bf90851cb 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java @@ -76,30 +76,32 @@ void test_validInboundSQSEvent() throws IOException { @Test void test_invalidInboundSQSEvent() throws IOException { - InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_alb_in_event.json"); - String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + try (InputStream is = this.getClass().getResourceAsStream("/validation/invalid_alb_in_event.json")) { + String invalidEvent = IOUtils.toString(is, StandardCharsets.UTF_8); - // WHEN - InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); - // THEN - // invocation should fail inbound validation and return an error message - JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); - assertThat(validJsonNode.get("errorMessage").asText()).contains(": required property 'price' not found"); + // THEN + // invocation should fail inbound validation and return an error message + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("errorMessage").asText()).contains(": required property 'price' not found"); + } } @Test void test_invalidOutboundSQSEvent() throws IOException { - InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_alb_out_event.json"); - String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + try (InputStream is = this.getClass().getResourceAsStream("/validation/invalid_alb_out_event.json")) { + String invalidEvent = IOUtils.toString(is, StandardCharsets.UTF_8); - // WHEN - InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); - // THEN - // invocation should fail outbound validation and return 400 - JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); - assertThat(validJsonNode.get("errorMessage").asText()) - .contains("/price: must have an exclusive maximum value of 1000"); + // THEN + // invocation should fail outbound validation and return 400 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("errorMessage").asText()) + .contains("/price: must have an exclusive maximum value of 1000"); + } } } diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java index e813d2965..2d1bf4657 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java @@ -61,47 +61,50 @@ static void tearDown() { @Test void test_validInboundApiGWEvent() throws IOException { - InputStream inputStream = this.getClass().getResourceAsStream("/validation/valid_api_gw_in_out_event.json"); - String validEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + try (InputStream is = this.getClass().getResourceAsStream("/validation/valid_api_gw_in_out_event.json")) { + String validEvent = IOUtils.toString(is, StandardCharsets.UTF_8); - // WHEN - InvocationResult invocationResult = invokeFunction(functionName, validEvent); + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, validEvent); - // THEN - // invocation should pass validation and return 200 - JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); - assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(200); - assertThat(validJsonNode.get("body").asText()).isEqualTo("{\"price\": 150}"); + // THEN + // invocation should pass validation and return 200 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(200); + assertThat(validJsonNode.get("body").asText()).isEqualTo("{\"price\": 150}"); + } } @Test void test_invalidInboundApiGWEvent() throws IOException { - InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_api_gw_in_event.json"); - String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + try (InputStream is = this.getClass().getResourceAsStream("/validation/invalid_api_gw_in_event.json")) { + String invalidEvent = IOUtils.toString(is, StandardCharsets.UTF_8); - // WHEN - InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); - // THEN - // invocation should fail inbound validation and return 400 - JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); - assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400); - assertThat(validJsonNode.get("body").asText()).contains(": required property 'price' not found"); + // THEN + // invocation should fail inbound validation and return 400 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400); + assertThat(validJsonNode.get("body").asText()).contains(": required property 'price' not found"); + } } @Test void test_invalidOutboundApiGWEvent() throws IOException { - InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_api_gw_out_event.json"); - String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - - // WHEN - InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); - - // THEN - // invocation should fail outbound validation and return 400 - JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); - assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400); - assertThat(validJsonNode.get("body").asText()) - .contains("/price: must have an exclusive maximum value of 1000"); + try (InputStream is = this.getClass().getResourceAsStream("/validation/invalid_api_gw_out_event.json")) { + String invalidEvent = IOUtils.toString(is, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); + + // THEN + // invocation should fail outbound validation and return 400 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400); + assertThat(validJsonNode.get("body").asText()) + .contains("/price: must have an exclusive maximum value of 1000"); + } } } From 2ad954e5cb3cc97a7349db6a9875798db985d738 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 5 Aug 2025 16:15:08 +0200 Subject: [PATCH 09/14] Fix more pmd_analyze issues. --- .../java/software/amazon/lambda/powertools/BatchE2ET.java | 5 ----- .../amazon/lambda/powertools/testutils/RetryUtils.java | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java index 28d5f9c24..181d5e583 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java @@ -269,14 +269,9 @@ private boolean areAllTestProductsPresent(ScanResponse items) { class Product { private long id; - private String name; - private double price; - public Product() { - } - public Product(long id, String name, double price) { this.id = id; this.name = name; diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/RetryUtils.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/RetryUtils.java index bab597036..054e9aa8e 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/RetryUtils.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/RetryUtils.java @@ -26,7 +26,7 @@ /** * Utility class for consistent retry configuration across all test utilities. */ -public class RetryUtils { +public final class RetryUtils { private static final Logger LOG = LoggerFactory.getLogger(RetryUtils.class); private static final RetryConfig DEFAULT_RETRY_CONFIG = RetryConfig.custom() From e5921d13bc287bc60d370c68dba5ff6cd3070d56 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 5 Aug 2025 16:19:42 +0200 Subject: [PATCH 10/14] Fix more pmd_analyze issues. --- .../software/amazon/lambda/powertools/TracingE2ET.java | 2 +- .../lambda/powertools/testutils/Infrastructure.java | 5 ++--- .../powertools/testutils/tracing/TraceFetcher.java | 9 ++++----- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java index 853899777..97b4df0a1 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java @@ -85,7 +85,7 @@ void test_tracing() { // We need to filter segments based on name because they are not returned in-order from the X-Ray API // The Init segment is created by default for Lambda functions in X-Ray final SubSegment initSegment = trace.getSubsegments().stream() - .filter(subSegment -> subSegment.getName().equals("Init")) + .filter(subSegment -> "Init".equals(subSegment.getName())) .findFirst().orElse(null); assertThat(initSegment.getName()).isEqualTo("Init"); assertThat(initSegment.getAnnotations()).isNull(); diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java index aa969a1b5..8822ef214 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java @@ -98,7 +98,7 @@ * `PutObjectRequest`) * and the CloudFormation stack is created (with the SDK `createStack`) */ -public class Infrastructure { +public final class Infrastructure { public static final String FUNCTION_NAME_OUTPUT = "functionName"; private static final Logger LOG = LoggerFactory.getLogger(Infrastructure.class); @@ -121,7 +121,6 @@ public class Infrastructure { private final String kinesisStream; private final String largeMessagesBucket; private String ddbStreamsTableName; - private String functionName; private Object cfnTemplate; private String cfnAssetDirectory; @@ -224,7 +223,7 @@ private Stack createStackWithLambda() { ? dockerConfig.createGraalVMBundlingOptions(pathToFunction, runtime) : dockerConfig.createJVMBundlingOptions(pathToFunction, runtime); - functionName = stackName + "-function"; + String functionName = stackName + "-function"; CfnOutput.Builder.create(e2eStack, FUNCTION_NAME_OUTPUT) .value(functionName) .build(); diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceFetcher.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceFetcher.java index 8da848da0..3653d42ad 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceFetcher.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceFetcher.java @@ -112,12 +112,11 @@ private Trace getTrace(List traceIds) { trace.segments().forEach(segment -> { try { SegmentDocument document = MAPPER.readValue(segment.document(), SegmentDocument.class); - if (document.getOrigin().equals("AWS::Lambda::Function")) { - if (document.hasSubsegments()) { - getNestedSubSegments(document.getSubsegments(), traceRes, - Collections.emptyList()); - } + if ("AWS::Lambda::Function".equals(document.getOrigin()) && document.hasSubsegments()) { + getNestedSubSegments(document.getSubsegments(), traceRes, + Collections.emptyList()); } + } catch (JsonProcessingException e) { LOG.error("Failed to parse segment document: " + e.getMessage()); throw new RuntimeException(e); From c87f91cb917a202b1b3eceef62e23f7e6ef0b92a Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 5 Aug 2025 16:24:30 +0200 Subject: [PATCH 11/14] Fix more pmd_analyze issues. --- .../java/software/amazon/lambda/powertools/TracingE2ET.java | 2 +- .../lambda/powertools/testutils/metrics/MetricsFetcher.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java index 97b4df0a1..4c5bc3ece 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java @@ -110,7 +110,7 @@ void test_tracing() { assertThat(sub.getName()).isIn("## internal_stuff", "## buildMessage"); SubSegment buildMessage = handleRequestSegment.getSubsegments().stream() - .filter(subSegment -> subSegment.getName().equals("## buildMessage")) + .filter(subSegment -> "## buildMessage".equals(subSegment.getName())) .findFirst().orElse(null); assertThat(buildMessage).isNotNull(); assertThat(buildMessage.getAnnotations()).hasSize(1); diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricsFetcher.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricsFetcher.java index b0768b43c..f856d8f2f 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricsFetcher.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricsFetcher.java @@ -17,6 +17,7 @@ import java.time.Instant; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.function.Supplier; @@ -76,7 +77,7 @@ public List fetchMetrics(Instant start, Instant end, int period, String .startTime(start) .endTime(end) .metricDataQueries(MetricDataQuery.builder() - .id(metricName.toLowerCase()) + .id(metricName.toLowerCase(Locale.ROOT)) .metricStat(MetricStat.builder() .unit(StandardUnit.COUNT) .metric(Metric.builder() From bb86e8f32e78c4864937161e850304145d0fb0c9 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 5 Aug 2025 16:26:21 +0200 Subject: [PATCH 12/14] Fix more pmd_analyze issues. --- .../amazon/lambda/powertools/testutils/Infrastructure.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java index 8822ef214..ea5ac3342 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java @@ -491,7 +491,7 @@ private Map findAssets() { return assets; } - public static class Builder { + public static final class Builder { public long timeoutInSeconds = 30; public String pathToFunction; public String testName; From 4bb03e7b77eebb8ead63859de012dc743f48e21f Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 5 Aug 2025 17:14:08 +0200 Subject: [PATCH 13/14] Add GraalVM support for TracingE2ET. --- powertools-e2e-tests/handlers/tracing/pom.xml | 24 +++++++- .../aws-lambda-java-core/reflect-config.json | 13 ++++ .../reflect-config.json | 35 +++++++++++ .../jni-config.json | 11 ++++ .../native-image.properties | 1 + .../reflect-config.json | 61 +++++++++++++++++++ .../resource-config.json | 19 ++++++ .../reflect-config.json | 25 ++++++++ .../reflect-config.json | 20 ++++++ .../resource-config.json | 7 +++ powertools-e2e-tests/pom.xml | 1 + .../amazon/lambda/powertools/TracingE2ET.java | 10 +-- .../testutils/tracing/TraceFetcher.java | 5 +- 13 files changed, 220 insertions(+), 12 deletions(-) create mode 100644 powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json create mode 100644 powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json create mode 100644 powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json create mode 100644 powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties create mode 100644 powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json create mode 100644 powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json create mode 100644 powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json create mode 100644 powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/reflect-config.json create mode 100644 powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/resource-config.json diff --git a/powertools-e2e-tests/handlers/tracing/pom.xml b/powertools-e2e-tests/handlers/tracing/pom.xml index b1bc14c05..67bcee662 100644 --- a/powertools-e2e-tests/handlers/tracing/pom.xml +++ b/powertools-e2e-tests/handlers/tracing/pom.xml @@ -1,5 +1,5 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 @@ -21,6 +21,14 @@ com.amazonaws aws-lambda-java-events + + com.amazonaws + aws-lambda-java-runtime-interface-client + + + com.amazonaws + aws-lambda-java-core + org.aspectj aspectjrt @@ -57,4 +65,18 @@ + + + + native-image + + + + org.graalvm.buildtools + native-maven-plugin + + + + + diff --git a/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json new file mode 100644 index 000000000..2780aca09 --- /dev/null +++ b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json @@ -0,0 +1,13 @@ +[ + { + "name":"com.amazonaws.services.lambda.runtime.LambdaRuntime", + "methods":[{"name":"","parameterTypes":[] }], + "fields":[{"name":"logger"}], + "allPublicMethods":true + }, + { + "name":"com.amazonaws.services.lambda.runtime.LambdaRuntimeInternal", + "methods":[{"name":"","parameterTypes":[] }], + "allPublicMethods":true + } +] \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json new file mode 100644 index 000000000..ddda5d5f1 --- /dev/null +++ b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json @@ -0,0 +1,35 @@ +[ + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent$ProxyRequestContext", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent$RequestIdentity", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + } +] \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json new file mode 100644 index 000000000..91be72f7a --- /dev/null +++ b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json @@ -0,0 +1,11 @@ +[ + { + "name":"com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeClientException", + "methods":[{"name":"","parameterTypes":["java.lang.String","int"] }] + }, + { + "name":"com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest", + "fields":[{"name":"id"}, {"name":"invokedFunctionArn"}, {"name":"deadlineTimeInMs"}, {"name":"xrayTraceId"}, {"name":"clientContext"}, {"name":"cognitoIdentity"}, {"name": "tenantId"}, {"name":"content"}], + "allPublicMethods":true + } +] \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties new file mode 100644 index 000000000..20f8b7801 --- /dev/null +++ b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties @@ -0,0 +1 @@ +Args = --initialize-at-build-time=jdk.xml.internal.SecuritySupport \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json new file mode 100644 index 000000000..e69fa735c --- /dev/null +++ b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json @@ -0,0 +1,61 @@ +[ + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.deser.Deserializers[]" + }, + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "com.amazonaws.services.lambda.runtime.LambdaRuntime", + "fields": [{ "name": "logger" }] + }, + { + "name": "com.amazonaws.services.lambda.runtime.logging.LogLevel", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true, + "allPublicFields": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.logging.LogFormat", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true, + "allPublicFields": true + }, + { + "name": "java.lang.Void", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "java.util.Collections$UnmodifiableMap", + "fields": [{ "name": "m" }] + }, + { + "name": "jdk.internal.module.IllegalAccessLogger", + "fields": [{ "name": "logger" }] + }, + { + "name": "sun.misc.Unsafe", + "fields": [{ "name": "theUnsafe" }] + }, + { + "name": "com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest", + "fields": [ + { "name": "id" }, + { "name": "invokedFunctionArn" }, + { "name": "deadlineTimeInMs" }, + { "name": "xrayTraceId" }, + { "name": "clientContext" }, + { "name": "cognitoIdentity" }, + { "name": "tenantId" }, + { "name": "content" } + ], + "allPublicMethods": true + } +] diff --git a/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json new file mode 100644 index 000000000..1062b4249 --- /dev/null +++ b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json @@ -0,0 +1,19 @@ +{ + "resources": { + "includes": [ + { + "pattern": "\\Qjni/libaws-lambda-jni.linux-aarch_64.so\\E" + }, + { + "pattern": "\\Qjni/libaws-lambda-jni.linux-x86_64.so\\E" + }, + { + "pattern": "\\Qjni/libaws-lambda-jni.linux_musl-aarch_64.so\\E" + }, + { + "pattern": "\\Qjni/libaws-lambda-jni.linux_musl-x86_64.so\\E" + } + ] + }, + "bundles": [] +} \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json new file mode 100644 index 000000000..9890688f9 --- /dev/null +++ b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json @@ -0,0 +1,25 @@ +[ + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.deser.Deserializers[]" + }, + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ext.Java7HandlersImpl", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ser.Serializers[]" + }, + { + "name": "org.joda.time.DateTime", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + } +] diff --git a/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/reflect-config.json b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/reflect-config.json new file mode 100644 index 000000000..9ddd235e2 --- /dev/null +++ b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/reflect-config.json @@ -0,0 +1,20 @@ +[ + { + "name": "software.amazon.lambda.powertools.e2e.Function", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name": "software.amazon.lambda.powertools.e2e.Input", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + } +] diff --git a/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/resource-config.json b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/resource-config.json new file mode 100644 index 000000000..be6aac3f6 --- /dev/null +++ b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/resource-config.json @@ -0,0 +1,7 @@ +{ + "resources":{ + "includes":[{ + "pattern":"\\Qlog4j2.xml\\E" + }]}, + "bundles":[] +} diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index 52abc2b45..ccd87e95e 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -242,6 +242,7 @@ **/MetricsE2ET.java **/LoggingE2ET.java **/ParametersE2ET.java + **/TracingE2ET.java true diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java index 4c5bc3ece..2b2e0d060 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java @@ -80,15 +80,7 @@ void test_tracing() { .build() .fetchTrace(); - assertThat(trace.getSubsegments()).hasSize(2); - - // We need to filter segments based on name because they are not returned in-order from the X-Ray API - // The Init segment is created by default for Lambda functions in X-Ray - final SubSegment initSegment = trace.getSubsegments().stream() - .filter(subSegment -> "Init".equals(subSegment.getName())) - .findFirst().orElse(null); - assertThat(initSegment.getName()).isEqualTo("Init"); - assertThat(initSegment.getAnnotations()).isNull(); + assertThat(trace.getSubsegments()).hasSize(1); final SubSegment handleRequestSegment = trace.getSubsegments().stream() .filter(subSegment -> "## handleRequest".equals(subSegment.getName())) diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceFetcher.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceFetcher.java index 3653d42ad..fc2d061cc 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceFetcher.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceFetcher.java @@ -171,7 +171,7 @@ public static class Builder { private Instant start; private Instant end; private String filterExpression; - private List excludedSegments = Arrays.asList("Initialization", "Invocation", "Overhead"); + private List excludedSegments = Arrays.asList("Initialization", "Init", "Invocation", "Overhead"); public TraceFetcher build() { if (filterExpression == null) { @@ -183,7 +183,8 @@ public TraceFetcher build() { if (end == null) { end = start.plus(1, ChronoUnit.MINUTES); } - LOG.debug("Looking for traces from {} to {} with filter {}", start, end, filterExpression); + LOG.debug("Looking for traces from {} to {} with filter {} and excluded segments {}", start, end, + filterExpression, excludedSegments); return new TraceFetcher(start, end, filterExpression, excludedSegments); } From 3bdb91ce230d214f735ab80df3d85c7d26fb5527 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 5 Aug 2025 17:16:46 +0200 Subject: [PATCH 14/14] Increase TracingE2ET timeout to 15 minutes. --- .../java/software/amazon/lambda/powertools/TracingE2ET.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java index 2b2e0d060..13d8adb9b 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java @@ -41,7 +41,7 @@ class TracingE2ET { private static String functionName; @BeforeAll - @Timeout(value = 10, unit = TimeUnit.MINUTES) + @Timeout(value = 15, unit = TimeUnit.MINUTES) static void setup() { infrastructure = Infrastructure.builder() .testName(TracingE2ET.class.getSimpleName())