diff --git a/.changes/next-release/bugfix-AWSSDKforJavav2-a6b451e.json b/.changes/next-release/bugfix-AWSSDKforJavav2-a6b451e.json new file mode 100644 index 000000000000..63e52f85ae71 --- /dev/null +++ b/.changes/next-release/bugfix-AWSSDKforJavav2-a6b451e.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Add business metrics tracking for precomputed checksum headers in requests." +} 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 2784f3e7fd48..c59a9c725697 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 @@ -86,7 +86,7 @@ public SdkHttpFullRequest.Builder execute(SdkHttpFullRequest.Builder request, Re result = legacyChecksum(request, context); } - recordChecksumBusinessMetrics(context.executionAttributes()); + recordChecksumBusinessMetrics(request, context.executionAttributes()); return result; } @@ -343,7 +343,7 @@ private PayloadChecksumStore getPayloadChecksumStore(ExecutionAttributes executi return executionAttributes.getAttribute(CHECKSUM_STORE); } - private void recordChecksumBusinessMetrics(ExecutionAttributes executionAttributes) { + private void recordChecksumBusinessMetrics(SdkHttpFullRequest.Builder request, ExecutionAttributes executionAttributes) { BusinessMetricCollection businessMetrics = executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS); @@ -360,10 +360,16 @@ private void recordChecksumBusinessMetrics(ExecutionAttributes executionAttribut .ifPresent(businessMetrics::addMetric); ChecksumSpecs checksumSpecs = executionAttributes.getAttribute(RESOLVED_CHECKSUM_SPECS); + ChecksumAlgorithm algorithm = resolveChecksumAlgorithm(checksumSpecs); + BusinessMetricsUtils.resolveChecksumAlgorithmFeatureIds(algorithm, request) + .forEach(businessMetrics::addMetric); + } + + private static ChecksumAlgorithm resolveChecksumAlgorithm(ChecksumSpecs checksumSpecs) { if (checksumSpecs != null && checksumSpecs.algorithmV2() != null) { - BusinessMetricsUtils.resolveChecksumAlgorithmMetric(checksumSpecs.algorithmV2()) - .ifPresent(businessMetrics::addMetric); + return checksumSpecs.algorithmV2(); } + return null; } static final class ChecksumCalculatingStreamProvider implements ContentStreamProvider { 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 162061e7cc94..49ac6e90416d 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 @@ -15,7 +15,9 @@ package software.amazon.awssdk.core.internal.useragent; +import java.util.HashSet; import java.util.Optional; +import java.util.Set; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.checksums.DefaultChecksumAlgorithm; import software.amazon.awssdk.checksums.spi.ChecksumAlgorithm; @@ -24,6 +26,7 @@ import software.amazon.awssdk.core.retry.RetryMode; import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; +import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.retries.AdaptiveRetryStrategy; import software.amazon.awssdk.retries.LegacyRetryStrategy; import software.amazon.awssdk.retries.StandardRetryStrategy; @@ -90,7 +93,22 @@ public static Optional resolveResponseChecksumValidationMetric( } } - public static Optional resolveChecksumAlgorithmMetric(ChecksumAlgorithm algorithm) { + public static Set resolveChecksumAlgorithmFeatureIds(ChecksumAlgorithm algorithm, + SdkHttpFullRequest.Builder request) { + Set ids = new HashSet<>(8); + request.forEachHeader((header, values) -> { + String id = headerToChecksumFeatureId(header); + if (id != null) { + ids.add(id); + } + }); + + resolveChecksumAlgorithmMetric(algorithm).ifPresent(ids::add); + + return ids; + } + + private static Optional resolveChecksumAlgorithmMetric(ChecksumAlgorithm algorithm) { if (algorithm == null) { return Optional.empty(); } @@ -135,4 +153,32 @@ public static Optional resolveChecksumAlgorithmMetric(ChecksumAlgorithm return Optional.empty(); } + // pkg private for testing + static String headerToChecksumFeatureId(String h) { + switch (h) { + case "x-amz-checksum-crc32": + return BusinessMetricFeatureId.FLEXIBLE_CHECKSUMS_REQ_CRC32.value(); + case "x-amz-checksum-crc32c": + return BusinessMetricFeatureId.FLEXIBLE_CHECKSUMS_REQ_CRC32C.value(); + case "x-amz-checksum-crc64nvme": + return BusinessMetricFeatureId.FLEXIBLE_CHECKSUMS_REQ_CRC64.value(); + case "x-amz-checksum-sha256": + return BusinessMetricFeatureId.FLEXIBLE_CHECKSUMS_REQ_SHA256.value(); + case "x-amz-checksum-sha512": + return BusinessMetricFeatureId.FLEXIBLE_CHECKSUMS_REQ_SHA512.value(); + case "x-amz-checksum-sha1": + return BusinessMetricFeatureId.FLEXIBLE_CHECKSUMS_REQ_SHA1.value(); + case "x-amz-checksum-md5": + return BusinessMetricFeatureId.FLEXIBLE_CHECKSUMS_REQ_MD5.value(); + case "x-amz-checksum-xxhash64": + return BusinessMetricFeatureId.FLEXIBLE_CHECKSUMS_REQ_XXHASH64.value(); + case "x-amz-checksum-xxhash3": + return BusinessMetricFeatureId.FLEXIBLE_CHECKSUMS_REQ_XXHASH3.value(); + case "x-amz-checksum-xxhash128": + return BusinessMetricFeatureId.FLEXIBLE_CHECKSUMS_REQ_XXHASH128.value(); + default: + return null; + } + } + } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/useragent/businessmetrics/BusinessMetricsUtilsTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/useragent/BusinessMetricsUtilsTest.java similarity index 51% rename from core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/useragent/businessmetrics/BusinessMetricsUtilsTest.java rename to core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/useragent/BusinessMetricsUtilsTest.java index 0922371c6df9..6b8018f64451 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/useragent/businessmetrics/BusinessMetricsUtilsTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/useragent/BusinessMetricsUtilsTest.java @@ -13,28 +13,40 @@ * permissions and limitations under the License. */ -package software.amazon.awssdk.core.internal.useragent.businessmetrics; +package software.amazon.awssdk.core.internal.useragent; import static org.assertj.core.api.Assertions.assertThat; +import java.util.Arrays; +import java.util.Locale; import java.util.Optional; 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.checksums.DefaultChecksumAlgorithm; +import software.amazon.awssdk.checksums.spi.ChecksumAlgorithm; import software.amazon.awssdk.core.internal.retry.SdkDefaultRetryStrategy; -import software.amazon.awssdk.core.internal.useragent.BusinessMetricsUtils; import software.amazon.awssdk.core.retry.RetryMode; import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; +import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.retries.api.RetryStrategy; class BusinessMetricsUtilsTest { + private SdkHttpFullRequest.Builder testRequest; + + @BeforeEach + void setup() { + testRequest = SdkHttpFullRequest.builder(); + } @ParameterizedTest(name = "{index} - {0}") - @MethodSource("inputValues") + @MethodSource("retryModeMetricInput") void when_retryModeMetric_isResolvedFromInput_correctMetricIsReturned(String description, RetryPolicy retryPolicy, - RetryStrategy retryStrategy, String expected) { + RetryStrategy retryStrategy, String expected) { Optional retryModeMetric = BusinessMetricsUtils.resolveRetryMode(retryPolicy, retryStrategy); if (expected != null) { @@ -44,7 +56,44 @@ void when_retryModeMetric_isResolvedFromInput_correctMetricIsReturned(String des } } - private static Stream inputValues() { + @ParameterizedTest(name = "{0} = {1}") + @MethodSource("checksumFeatureIdInput") + void when_checksumFeatureId_isResolvedFromHeader_correctMetricIsReturned(BusinessMetricFeatureId id, String header) { + assertThat(BusinessMetricsUtils.headerToChecksumFeatureId(header)).isEqualTo(id.value()); + } + + @Test + void when_checksumFeatureId_isResolvedFromHeader_unknownIsMappedToNull() { + assertThat(BusinessMetricsUtils.headerToChecksumFeatureId("x-amz-checksum-1234567")).isNull(); + } + + @Test + void when_checksumFeatureIds_areResolvedFromAlgorithmAndHeaders_allAlgorithmFeatureIdsReturned() { + ChecksumAlgorithm algorithm = DefaultChecksumAlgorithm.XXHASH128; + testRequest.putHeader("x-amz-checksum-crc32", "my-checksum"); + + assertThat(BusinessMetricsUtils.resolveChecksumAlgorithmFeatureIds(algorithm, testRequest)) + .containsExactly(BusinessMetricFeatureId.FLEXIBLE_CHECKSUMS_REQ_XXHASH128.value(), + BusinessMetricFeatureId.FLEXIBLE_CHECKSUMS_REQ_CRC32.value()); + } + + @Test + void when_checksumFeatureIds_areResolvedFromAlgorithmAndHeaders_andTheyResoveToTheSameId_idsAreDeduped() { + ChecksumAlgorithm algorithm = DefaultChecksumAlgorithm.CRC32; + testRequest.putHeader("x-amz-checksum-crc32", "my-checksum"); + + assertThat(BusinessMetricsUtils.resolveChecksumAlgorithmFeatureIds(algorithm, testRequest)) + .containsExactly(BusinessMetricFeatureId.FLEXIBLE_CHECKSUMS_REQ_CRC32.value()); + } + + @Test + void when_checksumFeatureIds_areResolvedFromAlgorithmAndHeaders_headerIsUnknown_ignored() { + testRequest.putHeader("x-amz-checksum-foo", "my-checksum"); + + assertThat(BusinessMetricsUtils.resolveChecksumAlgorithmFeatureIds(null, testRequest)).isEmpty(); + } + + private static Stream retryModeMetricInput() { return Stream.of( Arguments.of("No retry input returns empty", null, null, null), Arguments.of("Retry policy for legacy mode returns legacy", @@ -71,4 +120,19 @@ private static Stream inputValues() { BusinessMetricFeatureId.RETRY_MODE_LEGACY.value()) ); } + + private static Stream checksumFeatureIdInput() { + return Arrays.stream(BusinessMetricFeatureId.values()) + .filter(id -> id.name().startsWith("FLEXIBLE_CHECKSUMS_REQ_") + && !id.name().startsWith("FLEXIBLE_CHECKSUMS_REQ_WHEN")) + .map(id -> { + String name = id.name(); + String algorithm = name.substring(23).toLowerCase(Locale.US); + // CRC64 is special >_< + if ("crc64".equals(algorithm)) { + algorithm = "crc64nvme"; + } + return Arguments.of(id, "x-amz-checksum-" + algorithm); + }); + } } diff --git a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/customresponsemetadata/service-2.json b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/customresponsemetadata/service-2.json index d7b16e280513..084a22b018f6 100644 --- a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/customresponsemetadata/service-2.json +++ b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/customresponsemetadata/service-2.json @@ -1014,6 +1014,11 @@ "shape":"ChecksumAlgorithm", "location":"header", "locationName":"x-amz-sdk-checksum-algorithm" + }, + "ChecksumCrc32":{ + "shape":"String", + "location":"header", + "locationName":"x-amz-checksum-crc32" } }, "payload":"Body" 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 index 7e7794de62b2..7cf02a04ceef 100644 --- 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 @@ -70,6 +70,39 @@ public void setup() { mockHttpClient.stubNextResponse(mockResponse()); } + @Test + void when_precomputedChecksumProvided_correctMetricIsAdded() { + ProtocolRestJsonClient client = ProtocolRestJsonClient.builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS_PROVIDER) + .httpClient(mockHttpClient) + .build(); + + client.putOperationWithChecksum(r -> r.checksumCrc32("checksum"), + RequestBody.fromString("test content")); + + String userAgent = getUserAgentFromLastRequest(); + assertThat(userAgent).matches(METRIC_SEARCH_PATTERN.apply(FLEXIBLE_CHECKSUMS_REQ_CRC32.value())); + } + + @Test + void when_precomputedChecksum_and_checksumAlgProvided_correctMetricsAreAdded() { + ProtocolRestJsonClient client = ProtocolRestJsonClient.builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS_PROVIDER) + .httpClient(mockHttpClient) + .build(); + + client.putOperationWithChecksum(r -> r.checksumCrc32("checksum") + .checksumAlgorithm(ChecksumAlgorithm.CRC32_C), + RequestBody.fromString("test content")); + + String userAgent = getUserAgentFromLastRequest(); + + assertThat(userAgent).matches(METRIC_SEARCH_PATTERN.apply(FLEXIBLE_CHECKSUMS_REQ_CRC32.value())); + assertThat(userAgent).matches(METRIC_SEARCH_PATTERN.apply(FLEXIBLE_CHECKSUMS_REQ_CRC32C.value())); + } + @Test void when_noChecksumConfigurationIsSet_defaultConfigMetricsAreAdded() { ProtocolRestJsonClient client = ProtocolRestJsonClient.builder()