diff --git a/.changes/next-release/feature-AWSSDKforJavav2-c120920.json b/.changes/next-release/feature-AWSSDKforJavav2-c120920.json new file mode 100644 index 000000000000..894951d46e76 --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-c120920.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Adds business metrics for flexible checksum algorithms and configurations" +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/HttpChecksumStage.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/HttpChecksumStage.java index ca4b4d8f7f2c..c8521d3a09ef 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/HttpChecksumStage.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/HttpChecksumStage.java @@ -49,7 +49,9 @@ import software.amazon.awssdk.core.internal.http.RequestExecutionContext; import software.amazon.awssdk.core.internal.http.pipeline.MutableRequestToRequestPipeline; import software.amazon.awssdk.core.internal.io.AwsUnsignedChunkedEncodingInputStream; +import software.amazon.awssdk.core.internal.useragent.BusinessMetricsUtils; import software.amazon.awssdk.core.internal.util.HttpChecksumUtils; +import software.amazon.awssdk.core.useragent.BusinessMetricCollection; import software.amazon.awssdk.http.ContentStreamProvider; import software.amazon.awssdk.http.Header; import software.amazon.awssdk.http.SdkHttpFullRequest; @@ -79,11 +81,16 @@ public SdkHttpFullRequest.Builder execute(SdkHttpFullRequest.Builder request, Re ensurePayloadChecksumStorePresent(context.executionAttributes()); + SdkHttpFullRequest.Builder result; if (sraSigningEnabled(context)) { - return sraChecksum(request, context); + result = sraChecksum(request, context); + } else { + result = legacyChecksum(request, context); } - return legacyChecksum(request, context); + recordChecksumBusinessMetrics(context.executionAttributes()); + + return result; } private SdkHttpFullRequest.Builder legacyChecksum(SdkHttpFullRequest.Builder request, RequestExecutionContext context) { @@ -351,6 +358,29 @@ private PayloadChecksumStore getPayloadChecksumStore(ExecutionAttributes executi return executionAttributes.getAttribute(CHECKSUM_STORE); } + private void recordChecksumBusinessMetrics(ExecutionAttributes executionAttributes) { + BusinessMetricCollection businessMetrics = + executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS); + + if (businessMetrics == null) { + return; + } + + BusinessMetricsUtils.resolveRequestChecksumCalculationMetric( + executionAttributes.getAttribute(SdkInternalExecutionAttribute.REQUEST_CHECKSUM_CALCULATION)) + .ifPresent(businessMetrics::addMetric); + + BusinessMetricsUtils.resolveResponseChecksumValidationMetric( + executionAttributes.getAttribute(SdkInternalExecutionAttribute.RESPONSE_CHECKSUM_VALIDATION)) + .ifPresent(businessMetrics::addMetric); + + ChecksumSpecs checksumSpecs = executionAttributes.getAttribute(RESOLVED_CHECKSUM_SPECS); + if (checksumSpecs != null && checksumSpecs.algorithmV2() != null) { + BusinessMetricsUtils.resolveChecksumAlgorithmMetric(checksumSpecs.algorithmV2()) + .ifPresent(businessMetrics::addMetric); + } + } + static final class ChecksumCalculatingStreamProvider implements ContentStreamProvider { private final ContentStreamProvider underlyingInputStreamProvider; private final String checksumHeaderForTrailer; diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/BusinessMetricsUtils.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/BusinessMetricsUtils.java index ffff00586e93..17d242939d23 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/BusinessMetricsUtils.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/BusinessMetricsUtils.java @@ -17,6 +17,10 @@ import java.util.Optional; import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.checksums.DefaultChecksumAlgorithm; +import software.amazon.awssdk.checksums.spi.ChecksumAlgorithm; +import software.amazon.awssdk.core.checksums.RequestChecksumCalculation; +import software.amazon.awssdk.core.checksums.ResponseChecksumValidation; import software.amazon.awssdk.core.retry.RetryMode; import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; @@ -55,4 +59,59 @@ public static Optional resolveRetryMode(RetryPolicy retryPolicy, RetrySt } return Optional.empty(); } + + public static Optional resolveRequestChecksumCalculationMetric( + RequestChecksumCalculation requestChecksumCalculation) { + if (requestChecksumCalculation == null) { + return Optional.empty(); + } + switch (requestChecksumCalculation) { + case WHEN_SUPPORTED: + return Optional.of(BusinessMetricFeatureId.FLEXIBLE_CHECKSUMS_REQ_WHEN_SUPPORTED.value()); + case WHEN_REQUIRED: + return Optional.of(BusinessMetricFeatureId.FLEXIBLE_CHECKSUMS_REQ_WHEN_REQUIRED.value()); + default: + return Optional.empty(); + } + } + + public static Optional resolveResponseChecksumValidationMetric( + ResponseChecksumValidation responseChecksumValidation) { + if (responseChecksumValidation == null) { + return Optional.empty(); + } + switch (responseChecksumValidation) { + case WHEN_SUPPORTED: + return Optional.of(BusinessMetricFeatureId.FLEXIBLE_CHECKSUMS_RES_WHEN_SUPPORTED.value()); + case WHEN_REQUIRED: + return Optional.of(BusinessMetricFeatureId.FLEXIBLE_CHECKSUMS_RES_WHEN_REQUIRED.value()); + default: + return Optional.empty(); + } + } + + public static Optional resolveChecksumAlgorithmMetric(ChecksumAlgorithm algorithm) { + if (algorithm == null) { + return Optional.empty(); + } + + String algorithmId = algorithm.algorithmId(); + if (algorithmId.equals(DefaultChecksumAlgorithm.CRC32.algorithmId())) { + return Optional.of(BusinessMetricFeatureId.FLEXIBLE_CHECKSUMS_REQ_CRC32.value()); + } + if (algorithmId.equals(DefaultChecksumAlgorithm.CRC32C.algorithmId())) { + return Optional.of(BusinessMetricFeatureId.FLEXIBLE_CHECKSUMS_REQ_CRC32C.value()); + } + if (algorithmId.equals(DefaultChecksumAlgorithm.CRC64NVME.algorithmId())) { + return Optional.of(BusinessMetricFeatureId.FLEXIBLE_CHECKSUMS_REQ_CRC64.value()); + } + if (algorithmId.equals(DefaultChecksumAlgorithm.SHA1.algorithmId())) { + return Optional.of(BusinessMetricFeatureId.FLEXIBLE_CHECKSUMS_REQ_SHA1.value()); + } + if (algorithmId.equals(DefaultChecksumAlgorithm.SHA256.algorithmId())) { + return Optional.of(BusinessMetricFeatureId.FLEXIBLE_CHECKSUMS_REQ_SHA256.value()); + } + return Optional.empty(); + } + } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/useragent/BusinessMetricFeatureId.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/useragent/BusinessMetricFeatureId.java index 59ac9f7ebd1e..1778744c1e9a 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/useragent/BusinessMetricFeatureId.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/useragent/BusinessMetricFeatureId.java @@ -22,7 +22,7 @@ /** * An enum class representing a short form of identity providers to record in the UA string. * - * Unimplemented metrics: I,J,K,O,U-c + * Unimplemented metrics: I,J,K,O * Unsupported metrics (these will never be added): A,H */ @SdkProtectedApi @@ -42,6 +42,15 @@ public enum BusinessMetricFeatureId { ACCOUNT_ID_MODE_REQUIRED("R"), SIGV4A_SIGNING("S"), RESOLVED_ACCOUNT_ID("T"), + FLEXIBLE_CHECKSUMS_REQ_CRC32("U"), + FLEXIBLE_CHECKSUMS_REQ_CRC32C("V"), + FLEXIBLE_CHECKSUMS_REQ_CRC64("W"), + FLEXIBLE_CHECKSUMS_REQ_SHA1("X"), + FLEXIBLE_CHECKSUMS_REQ_SHA256("Y"), + FLEXIBLE_CHECKSUMS_REQ_WHEN_SUPPORTED("Z"), + FLEXIBLE_CHECKSUMS_REQ_WHEN_REQUIRED("a"), + FLEXIBLE_CHECKSUMS_RES_WHEN_SUPPORTED("b"), + FLEXIBLE_CHECKSUMS_RES_WHEN_REQUIRED("c"), DDB_MAPPER("d"), BEARER_SERVICE_ENV_VARS("3"), CREDENTIALS_CODE("e"), diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/FlexibleChecksumBusinessMetricTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/FlexibleChecksumBusinessMetricTest.java new file mode 100644 index 000000000000..6766d6df048a --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/FlexibleChecksumBusinessMetricTest.java @@ -0,0 +1,221 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.services; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.core.useragent.BusinessMetricCollection.METRIC_SEARCH_PATTERN; + +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.checksums.RequestChecksumCalculation; +import software.amazon.awssdk.core.checksums.ResponseChecksumValidation; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonClient; +import software.amazon.awssdk.services.protocolrestjson.model.ChecksumAlgorithm; +import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient; +import software.amazon.awssdk.utils.StringInputStream; + +/** + * Test class to verify that flexible checksum business metrics are correctly included + * in the User-Agent header when checksum algorithms are used. + */ +class FlexibleChecksumBusinessMetricTest { + private static final String USER_AGENT_HEADER_NAME = "User-Agent"; + private static final StaticCredentialsProvider CREDENTIALS_PROVIDER = + StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid")); + + private MockSyncHttpClient mockHttpClient; + + @BeforeEach + public void setup() { + mockHttpClient = new MockSyncHttpClient(); + mockHttpClient.stubNextResponse(mockResponse()); + } + + @Test + void when_noChecksumConfigurationIsSet_defaultConfigMetricsAreAdded() { + ProtocolRestJsonClient client = ProtocolRestJsonClient.builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS_PROVIDER) + .httpClient(mockHttpClient) + .build(); + + client.allTypes(r -> {}); + String userAgent = getUserAgentFromLastRequest(); + + assertThat(userAgent) + .matches(METRIC_SEARCH_PATTERN.apply("Z")) + .matches(METRIC_SEARCH_PATTERN.apply("b")) + .doesNotMatch(METRIC_SEARCH_PATTERN.apply("a")) + .doesNotMatch(METRIC_SEARCH_PATTERN.apply("c")); + } + + @ParameterizedTest + @MethodSource("checksumAlgorithmTestCases") + void when_checksumAlgorithmIsUsed_correctMetricIsAdded(ChecksumAlgorithm algorithm, String expectedMetric) { + ProtocolRestJsonClient client = ProtocolRestJsonClient.builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS_PROVIDER) + .httpClient(mockHttpClient) + .build(); + + client.putOperationWithChecksum(r -> r.checksumAlgorithm(algorithm), + RequestBody.fromString("test content")); + + String userAgent = getUserAgentFromLastRequest(); + assertThat(userAgent).matches(METRIC_SEARCH_PATTERN.apply(expectedMetric)); + } + + static Stream checksumAlgorithmTestCases() { + return Stream.of( + Arguments.of(ChecksumAlgorithm.CRC32, "U"), + Arguments.of(ChecksumAlgorithm.CRC32_C, "V"), + Arguments.of(ChecksumAlgorithm.CRC64_NVME, "W"), + Arguments.of(ChecksumAlgorithm.SHA1, "X"), + Arguments.of(ChecksumAlgorithm.SHA256, "Y") + ); + } + + @ParameterizedTest + @MethodSource("checksumConfigurationTestCases") + void when_checksumConfigurationIsSet_correctMetricIsAdded(RequestChecksumCalculation requestConfig, + ResponseChecksumValidation responseConfig, + String expectedRequestMetric, + String expectedResponseMetric) { + ProtocolRestJsonClient client = ProtocolRestJsonClient.builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS_PROVIDER) + .httpClient(mockHttpClient) + .requestChecksumCalculation(requestConfig) + .responseChecksumValidation(responseConfig) + .build(); + + client.allTypes(r -> {}); + + String userAgent = getUserAgentFromLastRequest(); + assertThat(userAgent) + .matches(METRIC_SEARCH_PATTERN.apply(expectedRequestMetric)) + .matches(METRIC_SEARCH_PATTERN.apply(expectedResponseMetric)); + } + + static Stream checksumConfigurationTestCases() { + return Stream.of( + Arguments.of(RequestChecksumCalculation.WHEN_SUPPORTED, + ResponseChecksumValidation.WHEN_SUPPORTED, "Z", "b"), + Arguments.of(RequestChecksumCalculation.WHEN_REQUIRED, + ResponseChecksumValidation.WHEN_REQUIRED, "a", "c"), + Arguments.of(RequestChecksumCalculation.WHEN_REQUIRED, + ResponseChecksumValidation.WHEN_SUPPORTED, "a", "b"), + Arguments.of(RequestChecksumCalculation.WHEN_SUPPORTED, + ResponseChecksumValidation.WHEN_REQUIRED, "Z", "c") + ); + } + + @ParameterizedTest + @MethodSource("checksumConfigurationWithAlgorithmTestCases") + void when_checksumConfigurationAndAlgorithmAreSet_correctMetricsAreAdded( + RequestChecksumCalculation requestConfig, + ResponseChecksumValidation responseConfig, + ChecksumAlgorithm algorithm, + String expectedRequestMetric, + String expectedResponseMetric, + String expectedAlgorithmMetric) { + + ProtocolRestJsonClient client = ProtocolRestJsonClient.builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS_PROVIDER) + .httpClient(mockHttpClient) + .requestChecksumCalculation(requestConfig) + .responseChecksumValidation(responseConfig) + .build(); + + client.putOperationWithChecksum(r -> r.checksumAlgorithm(algorithm), + RequestBody.fromString("test content")); + + String userAgent = getUserAgentFromLastRequest(); + + assertThat(userAgent) + .matches(METRIC_SEARCH_PATTERN.apply(expectedRequestMetric)) + .matches(METRIC_SEARCH_PATTERN.apply(expectedResponseMetric)) + .matches(METRIC_SEARCH_PATTERN.apply(expectedAlgorithmMetric)); + } + + static Stream checksumConfigurationWithAlgorithmTestCases() { + return Stream.of( + Arguments.of(RequestChecksumCalculation.WHEN_SUPPORTED, + ResponseChecksumValidation.WHEN_SUPPORTED, + ChecksumAlgorithm.CRC32, "Z", "b", "U"), + Arguments.of(RequestChecksumCalculation.WHEN_SUPPORTED, + ResponseChecksumValidation.WHEN_SUPPORTED, + ChecksumAlgorithm.CRC32_C, "Z", "b", "V"), + Arguments.of(RequestChecksumCalculation.WHEN_SUPPORTED, + ResponseChecksumValidation.WHEN_SUPPORTED, + ChecksumAlgorithm.SHA256, "Z", "b", "Y"), + + Arguments.of(RequestChecksumCalculation.WHEN_REQUIRED, + ResponseChecksumValidation.WHEN_REQUIRED, + ChecksumAlgorithm.CRC32, "a", "c", "U"), + Arguments.of(RequestChecksumCalculation.WHEN_REQUIRED, + ResponseChecksumValidation.WHEN_REQUIRED, + ChecksumAlgorithm.CRC64_NVME, "a", "c", "W"), + Arguments.of(RequestChecksumCalculation.WHEN_REQUIRED, + ResponseChecksumValidation.WHEN_REQUIRED, + ChecksumAlgorithm.SHA1, "a", "c", "X"), + + Arguments.of(RequestChecksumCalculation.WHEN_REQUIRED, + ResponseChecksumValidation.WHEN_SUPPORTED, + ChecksumAlgorithm.CRC32_C, "a", "b", "V"), + Arguments.of(RequestChecksumCalculation.WHEN_REQUIRED, + ResponseChecksumValidation.WHEN_SUPPORTED, + ChecksumAlgorithm.SHA256, "a", "b", "Y"), + + Arguments.of(RequestChecksumCalculation.WHEN_SUPPORTED, + ResponseChecksumValidation.WHEN_REQUIRED, + ChecksumAlgorithm.CRC64_NVME, "Z", "c", "W"), + Arguments.of(RequestChecksumCalculation.WHEN_SUPPORTED, + ResponseChecksumValidation.WHEN_REQUIRED, + ChecksumAlgorithm.SHA1, "Z", "c", "X") + ); + } + + private String getUserAgentFromLastRequest() { + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); + + List userAgentHeaders = lastRequest.headers().get(USER_AGENT_HEADER_NAME); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + return userAgentHeaders.get(0); + } + + private static HttpExecuteResponse mockResponse() { + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream("{}"))) + .build(); + } +}