From efb884c196022efdc4102b136a115bba03f68bcb Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Fri, 28 Mar 2025 18:03:41 +0100 Subject: [PATCH 1/5] Fix CDK Stack creation in e2e tests. --- powertools-e2e-tests/pom.xml | 6 +- .../powertools/testutils/Infrastructure.java | 99 ++++++++++--------- 2 files changed, 56 insertions(+), 49 deletions(-) diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index 1bf6310f4..03b6fe413 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -30,8 +30,8 @@ 11 11 - 10.3.0 - 2.162.1 + 10.4.2 + 2.186.0 @@ -231,4 +231,4 @@ - \ No newline at end of file + 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 bce4bbf98..143409989 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 @@ -16,7 +16,6 @@ import static java.util.Collections.singletonList; -import com.fasterxml.jackson.databind.JsonNode; import java.io.File; import java.io.IOException; import java.nio.file.Paths; @@ -27,13 +26,18 @@ import java.util.Objects; import java.util.UUID; import java.util.stream.Collectors; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yaml.snakeyaml.Yaml; + +import com.fasterxml.jackson.databind.JsonNode; + import software.amazon.awscdk.App; import software.amazon.awscdk.BundlingOptions; import software.amazon.awscdk.BundlingOutput; import software.amazon.awscdk.CfnOutput; +import software.amazon.awscdk.DefaultStackSynthesizer; import software.amazon.awscdk.DockerVolume; import software.amazon.awscdk.Duration; import software.amazon.awscdk.RemovalPolicy; @@ -45,7 +49,11 @@ import software.amazon.awscdk.services.appconfig.CfnDeploymentStrategy; import software.amazon.awscdk.services.appconfig.CfnEnvironment; import software.amazon.awscdk.services.appconfig.CfnHostedConfigurationVersion; -import software.amazon.awscdk.services.dynamodb.*; +import software.amazon.awscdk.services.dynamodb.Attribute; +import software.amazon.awscdk.services.dynamodb.AttributeType; +import software.amazon.awscdk.services.dynamodb.BillingMode; +import software.amazon.awscdk.services.dynamodb.StreamViewType; +import software.amazon.awscdk.services.dynamodb.Table; import software.amazon.awscdk.services.iam.PolicyStatement; import software.amazon.awscdk.services.kinesis.Stream; import software.amazon.awscdk.services.kinesis.StreamMode; @@ -89,7 +97,8 @@ * CloudWatch log groups, ... *
* It uses the Cloud Development Kit (CDK) to define required resources. The CDK stack is then synthesized to retrieve - * the CloudFormation templates and the assets (function jars). Assets are uploaded to S3 (with the SDK `PutObjectRequest`) + * the CloudFormation templates and the assets (function jars). Assets are uploaded to S3 (with the SDK + * `PutObjectRequest`) * and the CloudFormation stack is created (with the SDK `createStack`) */ public class Infrastructure { @@ -174,11 +183,11 @@ public Map deploy() { .onFailure(OnFailure.ROLLBACK) .capabilities(Capability.CAPABILITY_IAM) .build()); - WaiterResponse waiterResponse = - cfn.waiter().waitUntilStackCreateComplete(DescribeStacksRequest.builder().stackName(stackName).build()); + WaiterResponse waiterResponse = cfn.waiter() + .waitUntilStackCreateComplete(DescribeStacksRequest.builder().stackName(stackName).build()); if (waiterResponse.matched().response().isPresent()) { - software.amazon.awssdk.services.cloudformation.model.Stack deployedStack = - waiterResponse.matched().response().get().stacks().get(0); + software.amazon.awssdk.services.cloudformation.model.Stack deployedStack = waiterResponse.matched() + .response().get().stacks().get(0); LOG.info("Stack " + deployedStack.stackName() + " successfully deployed"); Map outputs = new HashMap<>(); deployedStack.outputs().forEach(output -> outputs.put(output.outputKey(), output.outputValue())); @@ -203,7 +212,11 @@ public void destroy() { */ private Stack createStackWithLambda() { boolean createTableForAsyncTests = false; - Stack stack = new Stack(app, stackName); + final Stack e2eStack = Stack.Builder.create(app, stackName) + .synthesizer(DefaultStackSynthesizer.Builder.create() + .generateBootstrapVersionRule(false) // Disable bootstrap version check + .build()) + .build(); List packagingInstruction = Arrays.asList( "/bin/sh", @@ -213,8 +226,7 @@ private Stack createStackWithLambda() { " -Dmaven.test.skip=true " + " -Dmaven.compiler.source=" + runtime.getMvnProperty() + " -Dmaven.compiler.target=" + runtime.getMvnProperty() + - " && cp /asset-input/" + pathToFunction + "/target/function.jar /asset-output/" - ); + " && cp /asset-input/" + pathToFunction + "/target/function.jar /asset-output/"); BundlingOptions.Builder builderOptions = BundlingOptions.builder() .command(packagingInstruction) @@ -224,20 +236,19 @@ private Stack createStackWithLambda() { DockerVolume.builder() .hostPath(System.getProperty("user.home") + "/.m2/") .containerPath("/root/.m2/") - .build() - )) + .build())) .user("root") .outputType(BundlingOutput.ARCHIVED); functionName = stackName + "-function"; - CfnOutput.Builder.create(stack, FUNCTION_NAME_OUTPUT) + CfnOutput.Builder.create(e2eStack, FUNCTION_NAME_OUTPUT) .value(functionName) .build(); LOG.debug("Building Lambda function with command " + packagingInstruction.stream().collect(Collectors.joining(" ", "[", "]"))); Function function = Function.Builder - .create(stack, functionName) + .create(e2eStack, functionName) .code(Code.fromAsset("handlers/", AssetOptions.builder() .bundling(builderOptions .command(packagingInstruction) @@ -253,7 +264,7 @@ private Stack createStackWithLambda() { .build(); LogGroup.Builder - .create(stack, functionName + "-logs") + .create(e2eStack, functionName + "-logs") .logGroupName("/aws/lambda/" + functionName) .retention(RetentionDays.ONE_DAY) .removalPolicy(RemovalPolicy.DESTROY) @@ -261,7 +272,7 @@ private Stack createStackWithLambda() { if (!StringUtils.isEmpty(idempotencyTable)) { Table table = Table.Builder - .create(stack, "IdempotencyTable") + .create(e2eStack, "IdempotencyTable") .billingMode(BillingMode.PAY_PER_REQUEST) .removalPolicy(RemovalPolicy.DESTROY) .partitionKey(Attribute.builder().name("id").type(AttributeType.STRING).build()) @@ -275,7 +286,7 @@ private Stack createStackWithLambda() { if (!StringUtils.isEmpty(queue)) { Queue sqsQueue = Queue.Builder - .create(stack, "SQSQueue") + .create(e2eStack, "SQSQueue") .queueName(queue) .visibilityTimeout(Duration.seconds(timeout * 6)) .retentionPeriod(Duration.seconds(timeout * 6)) @@ -293,14 +304,14 @@ private Stack createStackWithLambda() { .build(); function.addEventSource(sqsEventSource); CfnOutput.Builder - .create(stack, "QueueURL") + .create(e2eStack, "QueueURL") .value(sqsQueue.getQueueUrl()) .build(); createTableForAsyncTests = true; } if (!StringUtils.isEmpty(kinesisStream)) { Stream stream = Stream.Builder - .create(stack, "KinesisStream") + .create(e2eStack, "KinesisStream") .streamMode(StreamMode.ON_DEMAND) .streamName(kinesisStream) .build(); @@ -316,13 +327,13 @@ private Stack createStackWithLambda() { .build(); function.addEventSource(kinesisEventSource); CfnOutput.Builder - .create(stack, "KinesisStreamName") + .create(e2eStack, "KinesisStreamName") .value(stream.getStreamName()) .build(); } if (!StringUtils.isEmpty(ddbStreamsTableName)) { - Table ddbStreamsTable = Table.Builder.create(stack, "DDBStreamsTable") + Table ddbStreamsTable = Table.Builder.create(e2eStack, "DDBStreamsTable") .tableName(ddbStreamsTableName) .stream(StreamViewType.KEYS_ONLY) .removalPolicy(RemovalPolicy.DESTROY) @@ -336,12 +347,12 @@ private Stack createStackWithLambda() { .reportBatchItemFailures(true) .build(); function.addEventSource(ddbEventSource); - CfnOutput.Builder.create(stack, "DdbStreamsTestTable").value(ddbStreamsTable.getTableName()).build(); + CfnOutput.Builder.create(e2eStack, "DdbStreamsTestTable").value(ddbStreamsTable.getTableName()).build(); } if (!StringUtils.isEmpty(largeMessagesBucket)) { Bucket offloadBucket = Bucket.Builder - .create(stack, "LargeMessagesOffloadBucket") + .create(e2eStack, "LargeMessagesOffloadBucket") .removalPolicy(RemovalPolicy.RETAIN) // autodelete does not work without cdk deploy .bucketName(largeMessagesBucket) .build(); @@ -352,19 +363,19 @@ private Stack createStackWithLambda() { if (appConfig != null) { CfnApplication app = CfnApplication.Builder - .create(stack, "AppConfigApp") + .create(e2eStack, "AppConfigApp") .name(appConfig.getApplication()) .build(); CfnEnvironment environment = CfnEnvironment.Builder - .create(stack, "AppConfigEnvironment") + .create(e2eStack, "AppConfigEnvironment") .applicationId(app.getRef()) .name(appConfig.getEnvironment()) .build(); // Create a fast deployment strategy, so we don't have to wait ages CfnDeploymentStrategy fastDeployment = CfnDeploymentStrategy.Builder - .create(stack, "AppConfigDeployment") + .create(e2eStack, "AppConfigDeployment") .name("FastDeploymentStrategy") .deploymentDurationInMinutes(0) .finalBakeTimeInMinutes(0) @@ -377,20 +388,19 @@ private Stack createStackWithLambda() { .create() .actions(singletonList("appconfig:*")) .resources(singletonList("*")) - .build() - ); + .build()); CfnDeployment previousDeployment = null; for (Map.Entry entry : appConfig.getConfigurationValues().entrySet()) { CfnConfigurationProfile configProfile = CfnConfigurationProfile.Builder - .create(stack, "AppConfigProfileFor" + entry.getKey()) + .create(e2eStack, "AppConfigProfileFor" + entry.getKey()) .applicationId(app.getRef()) .locationUri("hosted") .name(entry.getKey()) .build(); CfnHostedConfigurationVersion configVersion = CfnHostedConfigurationVersion.Builder - .create(stack, "AppConfigHostedVersionFor" + entry.getKey()) + .create(e2eStack, "AppConfigHostedVersionFor" + entry.getKey()) .applicationId(app.getRef()) .contentType("text/plain") .configurationProfileId(configProfile.getRef()) @@ -398,7 +408,7 @@ private Stack createStackWithLambda() { .build(); CfnDeployment deployment = CfnDeployment.Builder - .create(stack, "AppConfigDepoymentFor" + entry.getKey()) + .create(e2eStack, "AppConfigDepoymentFor" + entry.getKey()) .applicationId(app.getRef()) .environmentId(environment.getRef()) .deploymentStrategyId(fastDeployment.getRef()) @@ -415,7 +425,7 @@ private Stack createStackWithLambda() { } if (createTableForAsyncTests) { Table table = Table.Builder - .create(stack, "TableForAsyncTests") + .create(e2eStack, "TableForAsyncTests") .billingMode(BillingMode.PAY_PER_REQUEST) .removalPolicy(RemovalPolicy.DESTROY) .partitionKey(Attribute.builder().name("functionName").type(AttributeType.STRING).build()) @@ -424,10 +434,10 @@ private Stack createStackWithLambda() { table.grantReadWriteData(function); function.addEnvironment("TABLE_FOR_ASYNC_TESTS", table.getTableName()); - CfnOutput.Builder.create(stack, "TableNameForAsyncTests").value(table.getTableName()).build(); + CfnOutput.Builder.create(e2eStack, "TableNameForAsyncTests").value(table.getTableName()).build(); } - return stack; + return e2eStack; } /** @@ -444,13 +454,13 @@ private void synthesize() { */ private void uploadAssets() { Map assets = findAssets(); - assets.forEach((objectKey, asset) -> - { + assets.forEach((objectKey, asset) -> { if (!asset.assetPath.endsWith(".jar")) { return; } - ListObjectsV2Response objects = - s3.listObjectsV2(ListObjectsV2Request.builder().bucket(asset.bucketName).build()); + + ListObjectsV2Response objects = s3 + .listObjectsV2(ListObjectsV2Request.builder().bucket(asset.bucketName).build()); if (objects.contents().stream().anyMatch(o -> o.key().equals(objectKey))) { LOG.debug("Asset already exists, skipping"); return; @@ -472,14 +482,13 @@ private Map findAssets() { JsonNode jsonNode = JsonConfig.get().getObjectMapper() .readTree(new File(cfnAssetDirectory, stackName + ".assets.json")); JsonNode files = jsonNode.get("files"); - files.iterator().forEachRemaining(file -> - { + 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(); + 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(); Asset asset = new Asset(assetPath, assetPackaging, bucketName.replace("${AWS::AccountId}", account) .replace("${AWS::Region}", region.toString())); assets.put(objectKey, asset); @@ -509,8 +518,6 @@ private Builder() { runtime = mapRuntimeVersion("JAVA_VERSION"); } - - private JavaRuntime mapRuntimeVersion(String environmentVariableName) { String javaVersion = System.getenv(environmentVariableName); // must be set in GitHub actions JavaRuntime ret = null; From 3b50767d1d9ed72987ac761cdf888adab6047a61 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Mon, 31 Mar 2025 17:17:48 +0200 Subject: [PATCH 2/5] Fix timezone problem in MetricsE2ET.java. --- .../amazon/lambda/powertools/MetricsE2ET.java | 4 +--- .../testutils/metrics/MetricsFetcher.java | 13 ++++++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/MetricsE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/MetricsE2ET.java index 235255dff..b6c3260f3 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/MetricsE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/MetricsE2ET.java @@ -71,9 +71,7 @@ public static void tearDown() { public void test_recordMetrics() { // GIVEN - Instant currentTimeTruncatedToMinutes = - LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES).toInstant(ZoneOffset.UTC); - + Instant currentTimeTruncatedToMinutes = Instant.now().truncatedTo(ChronoUnit.MINUTES); String event1 = "{ \"metrics\": {\"orders\": 1, \"products\": 4}, \"dimensions\": { \"Environment\": \"test\"}, \"highResolution\": \"false\"}"; 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 00728f451..186e72d13 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 @@ -54,7 +54,8 @@ public class MetricsFetcher { .build(); /** - * Retrieve the metric values from start to end. Different parameters are required (see {@link CloudWatchClient#getMetricData} for more info). + * Retrieve the metric values from start to end. Different parameters are required (see + * {@link CloudWatchClient#getMetricData} for more info). * Use a retry mechanism as metrics may not be available instantaneously after a function runs. * * @param start @@ -66,14 +67,13 @@ public class MetricsFetcher { * @return */ public List fetchMetrics(Instant start, Instant end, int period, String namespace, String metricName, - Map dimensions) { + Map dimensions) { List dimensionsList = new ArrayList<>(); if (dimensions != null) { dimensions.forEach((key, value) -> dimensionsList.add(Dimension.builder().name(key).value(value).build())); } - Callable> callable = () -> - { + Callable> callable = () -> { LOG.debug("Get Metrics for namespace {}, start {}, end {}, metric {}, dimensions {}", namespace, start, end, metricName, dimensionsList); GetMetricDataResponse metricData = cloudwatch.getMetricData(GetMetricDataRequest.builder() @@ -109,9 +109,8 @@ public List fetchMetrics(Instant start, Instant end, int period, String .build(); CallExecutor> callExecutor = new CallExecutorBuilder>() .config(retryConfig) - .afterFailedTryListener(s -> - { - LOG.warn(s.getLastExceptionThatCausedRetry().getMessage() + ", attempts: " + s.getTotalTries()); + .afterFailedTryListener(s -> { + LOG.warn("{}, attempts: {}", s.getLastExceptionThatCausedRetry().getMessage(), s.getTotalTries()); }) .build(); Status> status = callExecutor.execute(callable); From 758ffc6562d9292b433995080155dd94ea65e082 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Tue, 1 Apr 2025 11:50:26 +0200 Subject: [PATCH 3/5] Fix tracing E2E tests. --- .../lambda/powertools/e2e/Function.java | 2 +- .../amazon/lambda/powertools/TracingE2ET.java | 66 ++++++++++++------- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/powertools-e2e-tests/handlers/tracing/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/tracing/src/main/java/software/amazon/lambda/powertools/e2e/Function.java index 397e34a85..0f140a20d 100644 --- a/powertools-e2e-tests/handlers/tracing/src/main/java/software/amazon/lambda/powertools/e2e/Function.java +++ b/powertools-e2e-tests/handlers/tracing/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -52,4 +52,4 @@ private String buildMessage(String message, String funcName) { } return String.format("%s (%s)", message, funcName); } -} \ No newline at end of file +} 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 0827d91ae..d2a5ceed1 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 @@ -18,14 +18,15 @@ import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT; import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; -import java.util.Collections; import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; + 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; import software.amazon.lambda.powertools.testutils.tracing.SegmentDocument.SubSegment; @@ -45,7 +46,9 @@ public static void setup() { .testName(TracingE2ET.class.getSimpleName()) .pathToFunction("tracing") .tracing(true) - .environmentVariables(Collections.singletonMap("POWERTOOLS_SERVICE_NAME", service)) + .environmentVariables( + Map.of("POWERTOOLS_SERVICE_NAME", service, + "POWERTOOLS_TRACER_CAPTURE_RESPONSE", "true")) .build(); Map outputs = infrastructure.deploy(); functionName = outputs.get(FUNCTION_NAME_OUTPUT); @@ -61,45 +64,58 @@ public static void tearDown() { @Test public void test_tracing() { // GIVEN - String message = "Hello World"; - String event = String.format("{\"message\":\"%s\"}", message); - String result = String.format("%s (%s)", message, functionName); + final String message = "Hello World"; + final String event = String.format("{\"message\":\"%s\"}", message); + final String result = String.format("%s (%s)", message, functionName); // WHEN - InvocationResult invocationResult = invokeFunction(functionName, event); + final InvocationResult invocationResult = invokeFunction(functionName, event); // THEN - Trace trace = TraceFetcher.builder() + final Trace trace = TraceFetcher.builder() .start(invocationResult.getStart()) .end(invocationResult.getEnd()) .functionName(functionName) .build() .fetchTrace(); - assertThat(trace.getSubsegments()).hasSize(1); - SubSegment handleRequest = trace.getSubsegments().get(0); - assertThat(handleRequest.getName()).isEqualTo("## handleRequest"); - assertThat(handleRequest.getAnnotations()).hasSize(2); - assertThat(handleRequest.getAnnotations().get("ColdStart")).isEqualTo(true); - assertThat(handleRequest.getAnnotations().get("Service")).isEqualTo(service); - assertThat(handleRequest.getMetadata()).hasSize(1); - Map metadata = (Map) handleRequest.getMetadata().get(service); - assertThat(metadata.get("handleRequest response")).isEqualTo(result); - assertThat(handleRequest.getSubsegments()).hasSize(2); - - SubSegment sub = handleRequest.getSubsegments().get(0); + 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 -> subSegment.getName().equals("Init")) + .findFirst().orElse(null); + assertThat(initSegment.getName()).isEqualTo("Init"); + assertThat(initSegment.getAnnotations()).isNull(); + + final SubSegment handleRequestSegment = trace.getSubsegments().stream() + .filter(subSegment -> subSegment.getName().equals("## handleRequest")) + .findFirst().orElse(null); + assertThat(handleRequestSegment.getName()).isEqualTo("## handleRequest"); + assertThat(handleRequestSegment.getAnnotations()).hasSize(2); + assertThat(handleRequestSegment.getAnnotations()).containsEntry("ColdStart", true); + assertThat(handleRequestSegment.getAnnotations()).containsEntry("Service", service); + assertThat(handleRequestSegment.getMetadata()).hasSize(1); + final Map metadata = (Map) handleRequestSegment.getMetadata().get(service); + assertThat(metadata).containsEntry("handleRequest response", result); + assertThat(handleRequestSegment.getSubsegments()).hasSize(2); + + SubSegment sub = handleRequestSegment.getSubsegments().get(0); assertThat(sub.getName()).isIn("## internal_stuff", "## buildMessage"); - sub = handleRequest.getSubsegments().get(1); + sub = handleRequestSegment.getSubsegments().get(1); assertThat(sub.getName()).isIn("## internal_stuff", "## buildMessage"); - SubSegment buildMessage = handleRequest.getSubsegments().stream() - .filter(subSegment -> subSegment.getName().equals("## buildMessage")).findFirst().orElse(null); + SubSegment buildMessage = handleRequestSegment.getSubsegments().stream() + .filter(subSegment -> subSegment.getName().equals("## buildMessage")) + .findFirst().orElse(null); assertThat(buildMessage).isNotNull(); assertThat(buildMessage.getAnnotations()).hasSize(1); - assertThat(buildMessage.getAnnotations().get("message")).isEqualTo(message); + assertThat(buildMessage.getAnnotations()).containsEntry("message", message); assertThat(buildMessage.getMetadata()).hasSize(1); - metadata = (Map) buildMessage.getMetadata().get(service); - assertThat(metadata.get("buildMessage response")).isEqualTo(result); + final Map buildMessageSegmentMetadata = (Map) buildMessage.getMetadata() + .get(service); + assertThat(buildMessageSegmentMetadata).containsEntry("buildMessage response", result); } } From 37ebad533c876a4529495ca7d04f06a38dcba7e6 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 2 Apr 2025 14:07:36 +0200 Subject: [PATCH 4/5] Consistently use UTC time in MetricsE2ET. --- .../amazon/lambda/powertools/MetricsE2ET.java | 65 +++++++++---------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/MetricsE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/MetricsE2ET.java index b6c3260f3..2765e0e70 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/MetricsE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/MetricsE2ET.java @@ -18,9 +18,8 @@ import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT; import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; +import java.time.Clock; import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.List; @@ -29,10 +28,12 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; + 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; import software.amazon.lambda.powertools.testutils.metrics.MetricsFetcher; @@ -51,9 +52,9 @@ public static void setup() { .pathToFunction("metrics") .environmentVariables( Stream.of(new String[][] { - {"POWERTOOLS_METRICS_NAMESPACE", namespace}, - {"POWERTOOLS_SERVICE_NAME", service} - }) + { "POWERTOOLS_METRICS_NAMESPACE", namespace }, + { "POWERTOOLS_SERVICE_NAME", service } + }) .collect(Collectors.toMap(data -> data[0], data -> data[1]))) .build(); Map outputs = infrastructure.deploy(); @@ -71,12 +72,10 @@ public static void tearDown() { public void test_recordMetrics() { // GIVEN - Instant currentTimeTruncatedToMinutes = Instant.now().truncatedTo(ChronoUnit.MINUTES); - String event1 = - "{ \"metrics\": {\"orders\": 1, \"products\": 4}, \"dimensions\": { \"Environment\": \"test\"}, \"highResolution\": \"false\"}"; + Instant currentTimeTruncatedToMinutes = Instant.now(Clock.systemUTC()).truncatedTo(ChronoUnit.MINUTES); + String event1 = "{ \"metrics\": {\"orders\": 1, \"products\": 4}, \"dimensions\": { \"Environment\": \"test\"}, \"highResolution\": \"false\"}"; - String event2 = - "{ \"metrics\": {\"orders\": 1, \"products\": 8}, \"dimensions\": { \"Environment\": \"test\"}, \"highResolution\": \"true\"}"; + String event2 = "{ \"metrics\": {\"orders\": 1, \"products\": 8}, \"dimensions\": { \"Environment\": \"test\"}, \"highResolution\": \"true\"}"; // WHEN InvocationResult invocationResult = invokeFunction(functionName, event1); @@ -84,45 +83,43 @@ public void test_recordMetrics() { // THEN MetricsFetcher metricsFetcher = new MetricsFetcher(); - List coldStart = - metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, - "ColdStart", Stream.of(new String[][] { - {"FunctionName", functionName}, - {"Service", service}} - ).collect(Collectors.toMap(data -> data[0], data -> data[1]))); + List coldStart = metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, + namespace, + "ColdStart", Stream.of(new String[][] { + { "FunctionName", functionName }, + { "Service", service } }).collect(Collectors.toMap(data -> data[0], data -> data[1]))); assertThat(coldStart.get(0)).isEqualTo(1); - List orderMetrics = - metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, - "orders", Collections.singletonMap("Environment", "test")); + List orderMetrics = metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), + 60, namespace, + "orders", Collections.singletonMap("Environment", "test")); assertThat(orderMetrics.get(0)).isEqualTo(2); - List productMetrics = - metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, - "products", Collections.singletonMap("Environment", "test")); + List productMetrics = metricsFetcher.fetchMetrics(invocationResult.getStart(), + invocationResult.getEnd(), 60, namespace, + "products", Collections.singletonMap("Environment", "test")); - // When searching across a 1 minute time period with a period of 60 we find both metrics and the sum is 12 + // When searching across a 1 minute time period with a period of 60 we find both metrics and the sum is 12 assertThat(productMetrics.get(0)).isEqualTo(12); - orderMetrics = - metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, - "orders", Collections.singletonMap("Service", service)); + orderMetrics = metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, + namespace, + "orders", Collections.singletonMap("Service", service)); assertThat(orderMetrics.get(0)).isEqualTo(2); - productMetrics = - metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, namespace, - "products", Collections.singletonMap("Service", service)); + productMetrics = metricsFetcher.fetchMetrics(invocationResult.getStart(), invocationResult.getEnd(), 60, + namespace, + "products", Collections.singletonMap("Service", service)); assertThat(productMetrics.get(0)).isEqualTo(12); Instant searchStartTime = currentTimeTruncatedToMinutes.plusSeconds(15); Instant searchEndTime = currentTimeTruncatedToMinutes.plusSeconds(45); - List productMetricDataResult = - metricsFetcher.fetchMetrics(searchStartTime, searchEndTime, 1, namespace, - "products", Collections.singletonMap("Environment", "test")); + List productMetricDataResult = metricsFetcher.fetchMetrics(searchStartTime, searchEndTime, 1, namespace, + "products", Collections.singletonMap("Environment", "test")); -// We are searching across the time period the metric was created but with a period of 1 second. Only the high resolution metric will be available at this point + // We are searching across the time period the metric was created but with a period of 1 second. Only the high + // resolution metric will be available at this point assertThat(productMetricDataResult.get(0)).isEqualTo(8); - } } From e43ef44edb4abe5a47194df8c6e0b8fc7dbbef8f Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 2 Apr 2025 14:23:38 +0200 Subject: [PATCH 5/5] Fix LargeMessageE2ET. Initialize ExtendedSQSClient with correct region and use Autoclosable. --- .../lambda/powertools/LargeMessageE2ET.java | 99 ++++++++++--------- 1 file changed, 51 insertions(+), 48 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 548a710b8..d9c3ef749 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 @@ -3,8 +3,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT; -import com.amazon.sqs.javamessaging.AmazonSQSExtendedClient; -import com.amazon.sqs.javamessaging.ExtendedClientConfiguration; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -14,6 +12,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.AfterEach; @@ -22,6 +21,10 @@ import org.junit.jupiter.api.Timeout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import com.amazon.sqs.javamessaging.AmazonSQSExtendedClient; +import com.amazon.sqs.javamessaging.ExtendedClientConfiguration; + import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; import software.amazon.awssdk.regions.Region; @@ -101,21 +104,20 @@ public void reset() { @Test public void bigSQSMessageOffloadedToS3_shouldLoadFromS3() throws IOException, InterruptedException { // given - final ExtendedClientConfiguration extendedClientConfig = - new ExtendedClientConfiguration() - .withPayloadSupportEnabled(s3Client, bucketName); - AmazonSQSExtendedClient client = - new AmazonSQSExtendedClient(SqsClient.builder().httpClient(httpClient).build(), extendedClientConfig); - InputStream inputStream = this.getClass().getResourceAsStream("/large_sqs_message.txt"); - String bigMessage = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - - // when - client.sendMessage(SendMessageRequest - .builder() - .queueUrl(queueUrl) - .messageBody(bigMessage) - .build()); - + 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"); + String bigMessage = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // when + client.sendMessage(SendMessageRequest + .builder() + .queueUrl(queueUrl) + .messageBody(bigMessage) + .build()); + } Thread.sleep(30000); // wait for function to be executed // then @@ -137,36 +139,37 @@ public void bigSQSMessageOffloadedToS3_shouldLoadFromS3() throws IOException, In @Test public void smallSQSMessage_shouldNotReadFromS3() throws IOException, InterruptedException { // given - final ExtendedClientConfiguration extendedClientConfig = - new ExtendedClientConfiguration() - .withPayloadSupportEnabled(s3Client, bucketName); - AmazonSQSExtendedClient client = - new AmazonSQSExtendedClient(SqsClient.builder().httpClient(httpClient).build(), extendedClientConfig); - String message = "Hello World"; - - // when - client.sendMessage(SendMessageRequest - .builder() - .queueUrl(queueUrl) - .messageBody(message) - .build()); - - Thread.sleep(30000); // wait for function to be executed - - // then - QueryRequest request = QueryRequest - .builder() - .tableName(tableName) - .keyConditionExpression("functionName = :func") - .expressionAttributeValues( - Collections.singletonMap(":func", AttributeValue.builder().s(functionName).build())) - .build(); - QueryResponse response = dynamoDbClient.query(request); - List> items = response.items(); - assertThat(items).hasSize(1); - messageId = items.get(0).get("id").s(); - assertThat(Integer.valueOf(items.get(0).get("bodySize").n())).isEqualTo( - message.getBytes(StandardCharsets.UTF_8).length); - assertThat(items.get(0).get("bodyMD5").s()).isEqualTo("b10a8db164e0754105b7a99be72e3fe5"); + final ExtendedClientConfiguration extendedClientConfig = new ExtendedClientConfiguration() + .withPayloadSupportEnabled(s3Client, bucketName); + try (AmazonSQSExtendedClient client = new AmazonSQSExtendedClient( + SqsClient.builder().region(region).httpClient(httpClient).build(), + extendedClientConfig)) { + String message = "Hello World"; + + // when + client.sendMessage(SendMessageRequest + .builder() + .queueUrl(queueUrl) + .messageBody(message) + .build()); + + Thread.sleep(30000); // wait for function to be executed + + // then + QueryRequest request = QueryRequest + .builder() + .tableName(tableName) + .keyConditionExpression("functionName = :func") + .expressionAttributeValues( + Collections.singletonMap(":func", AttributeValue.builder().s(functionName).build())) + .build(); + QueryResponse response = dynamoDbClient.query(request); + List> items = response.items(); + assertThat(items).hasSize(1); + messageId = items.get(0).get("id").s(); + assertThat(Integer.valueOf(items.get(0).get("bodySize").n())).isEqualTo( + message.getBytes(StandardCharsets.UTF_8).length); + assertThat(items.get(0).get("bodyMD5").s()).isEqualTo("b10a8db164e0754105b7a99be72e3fe5"); + } } }