Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/next-release/feature-AWSSDKforJavav2-aa60cd3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "feature",
"category": "Amazon S3",
"contributor": "",
"description": "Implemented business metrics tracking for S3_Express_Bucket (featureID \"J\") through User-Agent header."
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Comment thread
S-Saranya1 marked this conversation as resolved.
b.addStatement("$T.addS3ExpressBusinessMetricIfApplicable(executionAttributes)",
ClassName.get("software.amazon.awssdk.services.s3.internal.s3express", "S3ExpressUtils"));
}

return b.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
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.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;
Expand Down Expand Up @@ -57,4 +58,15 @@ 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 (executionAttributes != null && useS3Express(executionAttributes) && useS3ExpressAuthScheme(executionAttributes)) {
Comment thread
S-Saranya1 marked this conversation as resolved.
executionAttributes.getOptionalAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS)
.ifPresent(businessMetrics ->
businessMetrics.addMetric(BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value()));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* 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.List;
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.sync.RequestBody;
import software.amazon.awssdk.core.sync.ResponseTransformer;
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";

public static final UnaryOperator<String> METRIC_SEARCH_PATTERN =
metric -> ".*m/[a-zA-Z0-9+-,]*" + metric + ".*";

private MockSyncHttpClient mockHttpClient;
private S3Client s3Client;

@BeforeEach
void setup() {
// Mock HTTP client
mockHttpClient = new MockSyncHttpClient();

// Mock CreateSession response for S3 Express authentication
String createSessionResponse = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<CreateSessionResult>\n" +
" <Credentials>\n" +
" <SessionToken>mock-session-token</SessionToken>\n" +
" <SecretAccessKey>mock-secret-key</SecretAccessKey>\n" +
" <AccessKeyId>mock-access-key</AccessKeyId>\n" +
" <Expiration>2025-12-31T23:59:59Z</Expiration>\n" +
" </Credentials>\n" +
"</CreateSessionResult>";

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)
.build();
}

@Test
void putObject_whenS3ExpressBucket_shouldIncludeS3ExpressFeatureIdInUserAgent() {
PutObjectRequest putRequest = PutObjectRequest.builder()
.bucket(S3_EXPRESS_BUCKET)
.key(KEY)
.build();

s3Client.putObject(putRequest, RequestBody.fromString(CONTENTS));

SdkHttpRequest lastRequest = mockHttpClient.getLastRequest();
assertThat(lastRequest).isNotNull();

List<String> userAgentHeaders = lastRequest.headers().get("User-Agent");
assertThat(userAgentHeaders).isNotNull().hasSize(1);

assertThat(userAgentHeaders.get(0)).matches(METRIC_SEARCH_PATTERN.apply("J"));
}

@Test
void getObject_whenS3ExpressBucket_shouldIncludeS3ExpressFeatureIdInUserAgent() {
GetObjectRequest getRequest = GetObjectRequest.builder()
.bucket(S3_EXPRESS_BUCKET)
.key(KEY)
.build();

s3Client.getObject(getRequest, ResponseTransformer.toBytes());

SdkHttpRequest lastRequest = mockHttpClient.getLastRequest();
assertThat(lastRequest).isNotNull();

List<String> userAgentHeaders = lastRequest.headers().get("User-Agent");
assertThat(userAgentHeaders).isNotNull().hasSize(1);

assertThat(userAgentHeaders.get(0)).matches(METRIC_SEARCH_PATTERN.apply("J"));
}

@Test
void putObject_whenRegularS3Bucket_shouldNotIncludeS3ExpressFeatureIdInUserAgent() {
PutObjectRequest putRequest = PutObjectRequest.builder()
.bucket(REGULAR_BUCKET)
.key(KEY)
.build();

s3Client.putObject(putRequest, RequestBody.fromString(CONTENTS));

SdkHttpRequest lastRequest = mockHttpClient.getLastRequest();
assertThat(lastRequest).isNotNull();

List<String> userAgentHeaders = lastRequest.headers().get("User-Agent");
assertThat(userAgentHeaders).isNotNull().hasSize(1);

assertThat(userAgentHeaders.get(0)).doesNotMatch(METRIC_SEARCH_PATTERN.apply("J"));
}

@Test
void getObject_whenRegularS3Bucket_shouldNotIncludeS3ExpressFeatureIdInUserAgent() {
GetObjectRequest getRequest = GetObjectRequest.builder()
.bucket(REGULAR_BUCKET)
.key(KEY)
.build();

s3Client.getObject(getRequest, ResponseTransformer.toBytes());

SdkHttpRequest lastRequest = mockHttpClient.getLastRequest();
assertThat(lastRequest).isNotNull();

List<String> userAgentHeaders = lastRequest.headers().get("User-Agent");
assertThat(userAgentHeaders).isNotNull().hasSize(1);

assertThat(userAgentHeaders.get(0)).doesNotMatch(METRIC_SEARCH_PATTERN.apply("J"));
}

}
Loading