From 05a972ad46b5bdfbb32d396223aa2f9a0b05bef8 Mon Sep 17 00:00:00 2001 From: Saranya Somepalli Date: Mon, 8 Sep 2025 12:41:36 -0700 Subject: [PATCH 1/3] Feature ID implementation for S3 Express Bucket --- .../feature-AWSSDKforJavav2-aa60cd3.json | 6 + .../EndpointResolverInterceptorSpec.java | 6 + .../useragent/BusinessMetricFeatureId.java | 1 + .../s3/internal/s3express/S3ExpressUtils.java | 16 ++ .../s3/s3express/S3ExpressUserAgentTest.java | 256 ++++++++++++++++++ 5 files changed, 285 insertions(+) create mode 100644 .changes/next-release/feature-AWSSDKforJavav2-aa60cd3.json create mode 100644 services/s3/src/test/java/software/amazon/awssdk/services/s3/s3express/S3ExpressUserAgentTest.java diff --git a/.changes/next-release/feature-AWSSDKforJavav2-aa60cd3.json b/.changes/next-release/feature-AWSSDKforJavav2-aa60cd3.json new file mode 100644 index 000000000000..d82734e1ea2e --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-aa60cd3.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Implemented business metrics tracking for S3_Express_Bucket (featureID \"J\") through User-Agent header." +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java index 7bac753e88e5..83c81b97ad7d 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java @@ -921,6 +921,12 @@ private MethodSpec setMetricValuesMethod() { + "metrics -> endpoint.attribute($T.METRIC_VALUES).forEach(v -> metrics.addMetric(v)))", SdkInternalExecutionAttribute.class, AwsEndpointAttribute.class); b.endControlFlow(); + + if (endpointRulesSpecUtils.isS3()) { + b.addStatement("$T.addS3ExpressBusinessMetricIfApplicable(executionAttributes)", + ClassName.get("software.amazon.awssdk.services.s3.internal.s3express", "S3ExpressUtils")); + } + return b.build(); } } 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 7f1483d56895..005eb0f83d32 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 @@ -36,6 +36,7 @@ public enum BusinessMetricFeatureId { S3_TRANSFER("G"), GZIP_REQUEST_COMPRESSION("L"), //TODO(metrics): Not working, compression happens after header ENDPOINT_OVERRIDE("N"), + S3_EXPRESS_BUCKET("J"), ACCOUNT_ID_MODE_PREFERRED("P"), ACCOUNT_ID_MODE_DISABLED("Q"), ACCOUNT_ID_MODE_REQUIRED("R"), diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtils.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtils.java index b55200c5e536..9eac42f9bcd5 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtils.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtils.java @@ -21,6 +21,8 @@ import software.amazon.awssdk.core.SelectedAuthScheme; import software.amazon.awssdk.core.interceptor.ExecutionAttributes; import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute; +import software.amazon.awssdk.core.useragent.BusinessMetricCollection; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.endpoints.Endpoint; import software.amazon.awssdk.http.auth.spi.scheme.AuthSchemeOption; import software.amazon.awssdk.services.s3.endpoints.internal.KnownS3ExpressEndpointProperty; @@ -57,4 +59,18 @@ public static boolean useS3ExpressAuthScheme(ExecutionAttributes executionAttrib } return false; } + + /** + * Adds S3 Express business metric if applicable for the current operation. + */ + public static void addS3ExpressBusinessMetricIfApplicable(ExecutionAttributes executionAttributes) { + if (useS3Express(executionAttributes) && useS3ExpressAuthScheme(executionAttributes)) { + BusinessMetricCollection businessMetrics = + executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS); + + if (businessMetrics != null) { + businessMetrics.addMetric(BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value()); + } + } + } } diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/s3express/S3ExpressUserAgentTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/s3express/S3ExpressUserAgentTest.java new file mode 100644 index 000000000000..5d14554216ba --- /dev/null +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/s3express/S3ExpressUserAgentTest.java @@ -0,0 +1,256 @@ +/* + * 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.s3.s3express; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.core.sync.ResponseTransformer; +import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; +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.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient; +import software.amazon.awssdk.utils.StringInputStream; + +/** + * Unit test to verify that S3 Express operations include the correct business metric feature ID + * in the User-Agent header. + */ +public class S3ExpressUserAgentTest { + private static final String KEY = "test-feature-id.txt"; + private static final String CONTENTS = "test content for feature id validation"; + private static final String S3_EXPRESS_BUCKET = "my-test-bucket--use1-az4--x-s3"; + private static final String REGULAR_BUCKET = "my-test-bucket-regular"; + + private final UserAgentCapturingInterceptor userAgentInterceptor = new UserAgentCapturingInterceptor(); + private MockSyncHttpClient mockHttpClient; + private S3Client s3Client; + + @BeforeEach + void setup() { + // Mock HTTP client + mockHttpClient = new MockSyncHttpClient(); + + // Mock CreateSession response for S3 Express authentication + String createSessionResponse = "\n" + + "\n" + + " \n" + + " mock-session-token\n" + + " mock-secret-key\n" + + " mock-access-key\n" + + " 2025-12-31T23:59:59Z\n" + + " \n" + + ""; + + HttpExecuteResponse createSessionHttpResponse = HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(createSessionResponse))) + .build(); + + HttpExecuteResponse putResponse = HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(""))) + .build(); + + HttpExecuteResponse getResponse = HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(CONTENTS))) + .build(); + + mockHttpClient.stubResponses( + createSessionHttpResponse, // First CreateSession call for S3 Express bucket + putResponse, // PUT operation + createSessionHttpResponse, // Second CreateSession call for S3 Express bucket + getResponse, // GET operation + putResponse, // PUT operation for regular bucket + getResponse // GET operation for regular bucket + ); + + // S3 client with mocked HTTP client + s3Client = S3Client.builder() + .region(Region.US_EAST_1) + .credentialsProvider(AnonymousCredentialsProvider.create()) + .httpClient(mockHttpClient) + .overrideConfiguration(o -> o.addExecutionInterceptor(userAgentInterceptor)) + .build(); + + userAgentInterceptor.reset(); + } + + @Test + void putObject_whenS3ExpressBucket_shouldIncludeS3ExpressFeatureIdInUserAgent() { + PutObjectRequest putRequest = PutObjectRequest.builder() + .bucket(S3_EXPRESS_BUCKET) + .key(KEY) + .build(); + + s3Client.putObject(putRequest, RequestBody.fromString(CONTENTS)); + + List capturedUserAgents = userAgentInterceptor.getCapturedUserAgents(); + assertThat(capturedUserAgents).hasSize(2); // CreateSession + PutObject calls + + // The second User-Agent is from the actual PutObject call + String userAgent = capturedUserAgents.get(1); + assertThat(userAgent).isNotNull(); + + String expectedFeatureId = BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value(); + String businessMetrics = extractBusinessMetrics(userAgent); + + assertThat(businessMetrics).contains(expectedFeatureId); + assertThat(userAgent).contains(" m/" + businessMetrics); + } + + @Test + void getObject_whenS3ExpressBucket_shouldIncludeS3ExpressFeatureIdInUserAgent() { + GetObjectRequest getRequest = GetObjectRequest.builder() + .bucket(S3_EXPRESS_BUCKET) + .key(KEY) + .build(); + + s3Client.getObject(getRequest, ResponseTransformer.toBytes()); + + List capturedUserAgents = userAgentInterceptor.getCapturedUserAgents(); + assertThat(capturedUserAgents).hasSize(2); + + String userAgent = capturedUserAgents.get(1); + assertThat(userAgent).isNotNull(); + + String expectedFeatureId = BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value(); + String businessMetrics = extractBusinessMetrics(userAgent); + + assertThat(businessMetrics).isNotNull(); + assertThat(businessMetrics).contains(expectedFeatureId); + assertThat(userAgent).contains(" m/" + businessMetrics); + } + + @Test + void putObject_whenRegularS3Bucket_shouldNotIncludeS3ExpressFeatureIdInUserAgent() { + PutObjectRequest putRequest = PutObjectRequest.builder() + .bucket(REGULAR_BUCKET) + .key(KEY) + .build(); + + s3Client.putObject(putRequest, RequestBody.fromString(CONTENTS)); + + List capturedUserAgents = userAgentInterceptor.getCapturedUserAgents(); + assertThat(capturedUserAgents).hasSize(1); + + String userAgent = capturedUserAgents.get(0); + assertThat(userAgent).isNotNull(); + + String s3ExpressFeatureId = BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value(); + + String businessMetrics = extractBusinessMetrics(userAgent); + if (businessMetrics != null) { + assertThat(businessMetrics).doesNotContain(s3ExpressFeatureId); + } + + } + + @Test + void getObject_whenRegularS3Bucket_shouldNotIncludeS3ExpressFeatureIdInUserAgent() { + GetObjectRequest getRequest = GetObjectRequest.builder() + .bucket(REGULAR_BUCKET) + .key(KEY) + .build(); + + s3Client.getObject(getRequest, ResponseTransformer.toBytes()); + + List capturedUserAgents = userAgentInterceptor.getCapturedUserAgents(); + assertThat(capturedUserAgents).hasSize(1); + + String userAgent = capturedUserAgents.get(0); + assertThat(userAgent).isNotNull(); + + String s3ExpressFeatureId = BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value(); + + String businessMetrics = extractBusinessMetrics(userAgent); + if (businessMetrics != null) { + assertThat(businessMetrics).doesNotContain(s3ExpressFeatureId); + } + + } + + /** + * Extracts the business metrics section from a User-Agent string. + * Business metrics appear as "m/D,J" where D and J are feature IDs. + */ + private String extractBusinessMetrics(String userAgent) { + if (userAgent == null) { + return null; + } + + // Pattern to match business metrics: " m/feature1,feature2" + Pattern pattern = Pattern.compile(" m/([A-Za-z0-9+\\-,]+)"); + Matcher matcher = pattern.matcher(userAgent); + + if (matcher.find()) { + return matcher.group(1); + } + + return null; + } + + /** + * Interceptor to capture User-Agent headers from HTTP requests + */ + private static class UserAgentCapturingInterceptor implements ExecutionInterceptor { + private final List capturedUserAgents = new ArrayList<>(); + private final AtomicReference lastUserAgent = new AtomicReference<>(); + + @Override + public void beforeTransmission(Context.BeforeTransmission context, ExecutionAttributes executionAttributes) { + SdkHttpRequest httpRequest = context.httpRequest(); + List userAgentHeaders = httpRequest.headers().get("User-Agent"); + + if (userAgentHeaders != null && !userAgentHeaders.isEmpty()) { + String userAgent = userAgentHeaders.get(0); + capturedUserAgents.add(userAgent); + lastUserAgent.set(userAgent); + } + } + + public List getCapturedUserAgents() { + return new ArrayList<>(capturedUserAgents); + } + + public String getLastUserAgent() { + return lastUserAgent.get(); + } + + public void reset() { + capturedUserAgents.clear(); + lastUserAgent.set(null); + } + } +} From 2f9d303dbd16ebc5496af673ee0801865a24b7db Mon Sep 17 00:00:00 2001 From: Saranya Somepalli Date: Tue, 9 Sep 2025 14:19:09 -0700 Subject: [PATCH 2/3] Address PR feedback --- .../feature-AWSSDKforJavav2-aa60cd3.json | 2 +- .../s3/internal/s3express/S3ExpressUtils.java | 15 +- .../s3/s3express/S3ExpressUserAgentTest.java | 130 ++++-------------- 3 files changed, 32 insertions(+), 115 deletions(-) diff --git a/.changes/next-release/feature-AWSSDKforJavav2-aa60cd3.json b/.changes/next-release/feature-AWSSDKforJavav2-aa60cd3.json index d82734e1ea2e..e40e92d1fc46 100644 --- a/.changes/next-release/feature-AWSSDKforJavav2-aa60cd3.json +++ b/.changes/next-release/feature-AWSSDKforJavav2-aa60cd3.json @@ -1,6 +1,6 @@ { "type": "feature", - "category": "AWS SDK for Java v2", + "category": "AWS S3", "contributor": "", "description": "Implemented business metrics tracking for S3_Express_Bucket (featureID \"J\") through User-Agent header." } diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtils.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtils.java index 9eac42f9bcd5..53b99239d640 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtils.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtils.java @@ -63,14 +63,13 @@ public static boolean useS3ExpressAuthScheme(ExecutionAttributes executionAttrib /** * Adds S3 Express business metric if applicable for the current operation. */ - public static void addS3ExpressBusinessMetricIfApplicable(ExecutionAttributes executionAttributes) { - if (useS3Express(executionAttributes) && useS3ExpressAuthScheme(executionAttributes)) { - BusinessMetricCollection businessMetrics = - executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS); - - if (businessMetrics != null) { - businessMetrics.addMetric(BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value()); + public static void addS3ExpressBusinessMetricIfApplicable(ExecutionAttributes executionAttributes){ + if(executionAttributes != null && useS3Express(executionAttributes) && useS3ExpressAuthScheme(executionAttributes)){ + executionAttributes.getOptionalAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS) + .ifPresent(businessMetrics -> + businessMetrics.addMetric(BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value()) + ); } - } + } } diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/s3express/S3ExpressUserAgentTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/s3express/S3ExpressUserAgentTest.java index 5d14554216ba..4776ea0249d7 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/s3express/S3ExpressUserAgentTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/s3express/S3ExpressUserAgentTest.java @@ -17,20 +17,13 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicReference; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import java.util.function.UnaryOperator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; -import software.amazon.awssdk.core.interceptor.Context; -import software.amazon.awssdk.core.interceptor.ExecutionAttributes; -import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.core.sync.ResponseTransformer; -import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.http.AbortableInputStream; import software.amazon.awssdk.http.HttpExecuteResponse; import software.amazon.awssdk.http.SdkHttpRequest; @@ -51,8 +44,10 @@ public class S3ExpressUserAgentTest { private static final String CONTENTS = "test content for feature id validation"; private static final String S3_EXPRESS_BUCKET = "my-test-bucket--use1-az4--x-s3"; private static final String REGULAR_BUCKET = "my-test-bucket-regular"; + + public static final UnaryOperator METRIC_SEARCH_PATTERN = + metric -> ".*m/[a-zA-Z0-9+-,]*" + metric + ".*"; - private final UserAgentCapturingInterceptor userAgentInterceptor = new UserAgentCapturingInterceptor(); private MockSyncHttpClient mockHttpClient; private S3Client s3Client; @@ -101,10 +96,7 @@ void setup() { .region(Region.US_EAST_1) .credentialsProvider(AnonymousCredentialsProvider.create()) .httpClient(mockHttpClient) - .overrideConfiguration(o -> o.addExecutionInterceptor(userAgentInterceptor)) .build(); - - userAgentInterceptor.reset(); } @Test @@ -116,18 +108,13 @@ void putObject_whenS3ExpressBucket_shouldIncludeS3ExpressFeatureIdInUserAgent() s3Client.putObject(putRequest, RequestBody.fromString(CONTENTS)); - List capturedUserAgents = userAgentInterceptor.getCapturedUserAgents(); - assertThat(capturedUserAgents).hasSize(2); // CreateSession + PutObject calls - - // The second User-Agent is from the actual PutObject call - String userAgent = capturedUserAgents.get(1); - assertThat(userAgent).isNotNull(); + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); - String expectedFeatureId = BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value(); - String businessMetrics = extractBusinessMetrics(userAgent); + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); - assertThat(businessMetrics).contains(expectedFeatureId); - assertThat(userAgent).contains(" m/" + businessMetrics); + assertThat(userAgentHeaders.get(0)).matches(METRIC_SEARCH_PATTERN.apply("J")); } @Test @@ -139,18 +126,13 @@ void getObject_whenS3ExpressBucket_shouldIncludeS3ExpressFeatureIdInUserAgent() s3Client.getObject(getRequest, ResponseTransformer.toBytes()); - List capturedUserAgents = userAgentInterceptor.getCapturedUserAgents(); - assertThat(capturedUserAgents).hasSize(2); + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); - String userAgent = capturedUserAgents.get(1); - assertThat(userAgent).isNotNull(); + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); - String expectedFeatureId = BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value(); - String businessMetrics = extractBusinessMetrics(userAgent); - - assertThat(businessMetrics).isNotNull(); - assertThat(businessMetrics).contains(expectedFeatureId); - assertThat(userAgent).contains(" m/" + businessMetrics); + assertThat(userAgentHeaders.get(0)).matches(METRIC_SEARCH_PATTERN.apply("J")); } @Test @@ -162,19 +144,13 @@ void putObject_whenRegularS3Bucket_shouldNotIncludeS3ExpressFeatureIdInUserAgent s3Client.putObject(putRequest, RequestBody.fromString(CONTENTS)); - List capturedUserAgents = userAgentInterceptor.getCapturedUserAgents(); - assertThat(capturedUserAgents).hasSize(1); - - String userAgent = capturedUserAgents.get(0); - assertThat(userAgent).isNotNull(); + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); - String s3ExpressFeatureId = BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value(); - - String businessMetrics = extractBusinessMetrics(userAgent); - if (businessMetrics != null) { - assertThat(businessMetrics).doesNotContain(s3ExpressFeatureId); - } + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + assertThat(userAgentHeaders.get(0)).doesNotMatch(METRIC_SEARCH_PATTERN.apply("J")); } @Test @@ -186,71 +162,13 @@ void getObject_whenRegularS3Bucket_shouldNotIncludeS3ExpressFeatureIdInUserAgent s3Client.getObject(getRequest, ResponseTransformer.toBytes()); - List capturedUserAgents = userAgentInterceptor.getCapturedUserAgents(); - assertThat(capturedUserAgents).hasSize(1); - - String userAgent = capturedUserAgents.get(0); - assertThat(userAgent).isNotNull(); - - String s3ExpressFeatureId = BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value(); - - String businessMetrics = extractBusinessMetrics(userAgent); - if (businessMetrics != null) { - assertThat(businessMetrics).doesNotContain(s3ExpressFeatureId); - } + SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); + assertThat(lastRequest).isNotNull(); - } + List userAgentHeaders = lastRequest.headers().get("User-Agent"); + assertThat(userAgentHeaders).isNotNull().hasSize(1); - /** - * Extracts the business metrics section from a User-Agent string. - * Business metrics appear as "m/D,J" where D and J are feature IDs. - */ - private String extractBusinessMetrics(String userAgent) { - if (userAgent == null) { - return null; - } - - // Pattern to match business metrics: " m/feature1,feature2" - Pattern pattern = Pattern.compile(" m/([A-Za-z0-9+\\-,]+)"); - Matcher matcher = pattern.matcher(userAgent); - - if (matcher.find()) { - return matcher.group(1); - } - - return null; + assertThat(userAgentHeaders.get(0)).doesNotMatch(METRIC_SEARCH_PATTERN.apply("J")); } - /** - * Interceptor to capture User-Agent headers from HTTP requests - */ - private static class UserAgentCapturingInterceptor implements ExecutionInterceptor { - private final List capturedUserAgents = new ArrayList<>(); - private final AtomicReference lastUserAgent = new AtomicReference<>(); - - @Override - public void beforeTransmission(Context.BeforeTransmission context, ExecutionAttributes executionAttributes) { - SdkHttpRequest httpRequest = context.httpRequest(); - List userAgentHeaders = httpRequest.headers().get("User-Agent"); - - if (userAgentHeaders != null && !userAgentHeaders.isEmpty()) { - String userAgent = userAgentHeaders.get(0); - capturedUserAgents.add(userAgent); - lastUserAgent.set(userAgent); - } - } - - public List getCapturedUserAgents() { - return new ArrayList<>(capturedUserAgents); - } - - public String getLastUserAgent() { - return lastUserAgent.get(); - } - - public void reset() { - capturedUserAgents.clear(); - lastUserAgent.set(null); - } - } } From 662718dca9b8a7855b3f8f697e1cb1c769f10016 Mon Sep 17 00:00:00 2001 From: Saranya Somepalli Date: Wed, 10 Sep 2025 10:08:15 -0700 Subject: [PATCH 3/3] Fixing checkstyles --- .../feature-AWSSDKforJavav2-aa60cd3.json | 2 +- .../s3/internal/s3express/S3ExpressUtils.java | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.changes/next-release/feature-AWSSDKforJavav2-aa60cd3.json b/.changes/next-release/feature-AWSSDKforJavav2-aa60cd3.json index e40e92d1fc46..48c75e006610 100644 --- a/.changes/next-release/feature-AWSSDKforJavav2-aa60cd3.json +++ b/.changes/next-release/feature-AWSSDKforJavav2-aa60cd3.json @@ -1,6 +1,6 @@ { "type": "feature", - "category": "AWS S3", + "category": "Amazon S3", "contributor": "", "description": "Implemented business metrics tracking for S3_Express_Bucket (featureID \"J\") through User-Agent header." } diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtils.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtils.java index 53b99239d640..441e575687e4 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtils.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtils.java @@ -21,7 +21,6 @@ import software.amazon.awssdk.core.SelectedAuthScheme; import software.amazon.awssdk.core.interceptor.ExecutionAttributes; import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute; -import software.amazon.awssdk.core.useragent.BusinessMetricCollection; import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.endpoints.Endpoint; import software.amazon.awssdk.http.auth.spi.scheme.AuthSchemeOption; @@ -63,13 +62,11 @@ public static boolean useS3ExpressAuthScheme(ExecutionAttributes executionAttrib /** * Adds S3 Express business metric if applicable for the current operation. */ - public static void addS3ExpressBusinessMetricIfApplicable(ExecutionAttributes executionAttributes){ - if(executionAttributes != null && useS3Express(executionAttributes) && useS3ExpressAuthScheme(executionAttributes)){ - executionAttributes.getOptionalAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS) - .ifPresent(businessMetrics -> - businessMetrics.addMetric(BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value()) - ); - } - + public static void addS3ExpressBusinessMetricIfApplicable(ExecutionAttributes executionAttributes) { + if (executionAttributes != null && useS3Express(executionAttributes) && useS3ExpressAuthScheme(executionAttributes)) { + executionAttributes.getOptionalAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS) + .ifPresent(businessMetrics -> + businessMetrics.addMetric(BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value())); + } } }