Skip to content

Commit f8000dd

Browse files
authored
Feature ID implementation for S3 Express Bucket (#6409)
* Feature ID implementation for S3 Express Bucket * Address PR feedback * Fixing checkstyles
1 parent b4801aa commit f8000dd

File tree

5 files changed

+199
-0
lines changed

5 files changed

+199
-0
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "Amazon S3",
4+
"contributor": "",
5+
"description": "Implemented business metrics tracking for S3_Express_Bucket (featureID \"J\") through User-Agent header."
6+
}

codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -921,6 +921,12 @@ private MethodSpec setMetricValuesMethod() {
921921
+ "metrics -> endpoint.attribute($T.METRIC_VALUES).forEach(v -> metrics.addMetric(v)))",
922922
SdkInternalExecutionAttribute.class, AwsEndpointAttribute.class);
923923
b.endControlFlow();
924+
925+
if (endpointRulesSpecUtils.isS3()) {
926+
b.addStatement("$T.addS3ExpressBusinessMetricIfApplicable(executionAttributes)",
927+
ClassName.get("software.amazon.awssdk.services.s3.internal.s3express", "S3ExpressUtils"));
928+
}
929+
924930
return b.build();
925931
}
926932
}

core/sdk-core/src/main/java/software/amazon/awssdk/core/useragent/BusinessMetricFeatureId.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public enum BusinessMetricFeatureId {
3636
S3_TRANSFER("G"),
3737
GZIP_REQUEST_COMPRESSION("L"), //TODO(metrics): Not working, compression happens after header
3838
ENDPOINT_OVERRIDE("N"),
39+
S3_EXPRESS_BUCKET("J"),
3940
ACCOUNT_ID_MODE_PREFERRED("P"),
4041
ACCOUNT_ID_MODE_DISABLED("Q"),
4142
ACCOUNT_ID_MODE_REQUIRED("R"),

services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/s3express/S3ExpressUtils.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import software.amazon.awssdk.core.SelectedAuthScheme;
2222
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
2323
import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute;
24+
import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId;
2425
import software.amazon.awssdk.endpoints.Endpoint;
2526
import software.amazon.awssdk.http.auth.spi.scheme.AuthSchemeOption;
2627
import software.amazon.awssdk.services.s3.endpoints.internal.KnownS3ExpressEndpointProperty;
@@ -57,4 +58,15 @@ public static boolean useS3ExpressAuthScheme(ExecutionAttributes executionAttrib
5758
}
5859
return false;
5960
}
61+
62+
/**
63+
* Adds S3 Express business metric if applicable for the current operation.
64+
*/
65+
public static void addS3ExpressBusinessMetricIfApplicable(ExecutionAttributes executionAttributes) {
66+
if (executionAttributes != null && useS3Express(executionAttributes) && useS3ExpressAuthScheme(executionAttributes)) {
67+
executionAttributes.getOptionalAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS)
68+
.ifPresent(businessMetrics ->
69+
businessMetrics.addMetric(BusinessMetricFeatureId.S3_EXPRESS_BUCKET.value()));
70+
}
71+
}
6072
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.services.s3.s3express;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.util.List;
21+
import java.util.function.UnaryOperator;
22+
import org.junit.jupiter.api.BeforeEach;
23+
import org.junit.jupiter.api.Test;
24+
import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider;
25+
import software.amazon.awssdk.core.sync.RequestBody;
26+
import software.amazon.awssdk.core.sync.ResponseTransformer;
27+
import software.amazon.awssdk.http.AbortableInputStream;
28+
import software.amazon.awssdk.http.HttpExecuteResponse;
29+
import software.amazon.awssdk.http.SdkHttpRequest;
30+
import software.amazon.awssdk.http.SdkHttpResponse;
31+
import software.amazon.awssdk.regions.Region;
32+
import software.amazon.awssdk.services.s3.S3Client;
33+
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
34+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
35+
import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient;
36+
import software.amazon.awssdk.utils.StringInputStream;
37+
38+
/**
39+
* Unit test to verify that S3 Express operations include the correct business metric feature ID
40+
* in the User-Agent header.
41+
*/
42+
public class S3ExpressUserAgentTest {
43+
private static final String KEY = "test-feature-id.txt";
44+
private static final String CONTENTS = "test content for feature id validation";
45+
private static final String S3_EXPRESS_BUCKET = "my-test-bucket--use1-az4--x-s3";
46+
private static final String REGULAR_BUCKET = "my-test-bucket-regular";
47+
48+
public static final UnaryOperator<String> METRIC_SEARCH_PATTERN =
49+
metric -> ".*m/[a-zA-Z0-9+-,]*" + metric + ".*";
50+
51+
private MockSyncHttpClient mockHttpClient;
52+
private S3Client s3Client;
53+
54+
@BeforeEach
55+
void setup() {
56+
// Mock HTTP client
57+
mockHttpClient = new MockSyncHttpClient();
58+
59+
// Mock CreateSession response for S3 Express authentication
60+
String createSessionResponse = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
61+
"<CreateSessionResult>\n" +
62+
" <Credentials>\n" +
63+
" <SessionToken>mock-session-token</SessionToken>\n" +
64+
" <SecretAccessKey>mock-secret-key</SecretAccessKey>\n" +
65+
" <AccessKeyId>mock-access-key</AccessKeyId>\n" +
66+
" <Expiration>2025-12-31T23:59:59Z</Expiration>\n" +
67+
" </Credentials>\n" +
68+
"</CreateSessionResult>";
69+
70+
HttpExecuteResponse createSessionHttpResponse = HttpExecuteResponse.builder()
71+
.response(SdkHttpResponse.builder().statusCode(200).build())
72+
.responseBody(AbortableInputStream.create(new StringInputStream(createSessionResponse)))
73+
.build();
74+
75+
HttpExecuteResponse putResponse = HttpExecuteResponse.builder()
76+
.response(SdkHttpResponse.builder().statusCode(200).build())
77+
.responseBody(AbortableInputStream.create(new StringInputStream("")))
78+
.build();
79+
80+
HttpExecuteResponse getResponse = HttpExecuteResponse.builder()
81+
.response(SdkHttpResponse.builder().statusCode(200).build())
82+
.responseBody(AbortableInputStream.create(new StringInputStream(CONTENTS)))
83+
.build();
84+
85+
mockHttpClient.stubResponses(
86+
createSessionHttpResponse, // First CreateSession call for S3 Express bucket
87+
putResponse, // PUT operation
88+
createSessionHttpResponse, // Second CreateSession call for S3 Express bucket
89+
getResponse, // GET operation
90+
putResponse, // PUT operation for regular bucket
91+
getResponse // GET operation for regular bucket
92+
);
93+
94+
// S3 client with mocked HTTP client
95+
s3Client = S3Client.builder()
96+
.region(Region.US_EAST_1)
97+
.credentialsProvider(AnonymousCredentialsProvider.create())
98+
.httpClient(mockHttpClient)
99+
.build();
100+
}
101+
102+
@Test
103+
void putObject_whenS3ExpressBucket_shouldIncludeS3ExpressFeatureIdInUserAgent() {
104+
PutObjectRequest putRequest = PutObjectRequest.builder()
105+
.bucket(S3_EXPRESS_BUCKET)
106+
.key(KEY)
107+
.build();
108+
109+
s3Client.putObject(putRequest, RequestBody.fromString(CONTENTS));
110+
111+
SdkHttpRequest lastRequest = mockHttpClient.getLastRequest();
112+
assertThat(lastRequest).isNotNull();
113+
114+
List<String> userAgentHeaders = lastRequest.headers().get("User-Agent");
115+
assertThat(userAgentHeaders).isNotNull().hasSize(1);
116+
117+
assertThat(userAgentHeaders.get(0)).matches(METRIC_SEARCH_PATTERN.apply("J"));
118+
}
119+
120+
@Test
121+
void getObject_whenS3ExpressBucket_shouldIncludeS3ExpressFeatureIdInUserAgent() {
122+
GetObjectRequest getRequest = GetObjectRequest.builder()
123+
.bucket(S3_EXPRESS_BUCKET)
124+
.key(KEY)
125+
.build();
126+
127+
s3Client.getObject(getRequest, ResponseTransformer.toBytes());
128+
129+
SdkHttpRequest lastRequest = mockHttpClient.getLastRequest();
130+
assertThat(lastRequest).isNotNull();
131+
132+
List<String> userAgentHeaders = lastRequest.headers().get("User-Agent");
133+
assertThat(userAgentHeaders).isNotNull().hasSize(1);
134+
135+
assertThat(userAgentHeaders.get(0)).matches(METRIC_SEARCH_PATTERN.apply("J"));
136+
}
137+
138+
@Test
139+
void putObject_whenRegularS3Bucket_shouldNotIncludeS3ExpressFeatureIdInUserAgent() {
140+
PutObjectRequest putRequest = PutObjectRequest.builder()
141+
.bucket(REGULAR_BUCKET)
142+
.key(KEY)
143+
.build();
144+
145+
s3Client.putObject(putRequest, RequestBody.fromString(CONTENTS));
146+
147+
SdkHttpRequest lastRequest = mockHttpClient.getLastRequest();
148+
assertThat(lastRequest).isNotNull();
149+
150+
List<String> userAgentHeaders = lastRequest.headers().get("User-Agent");
151+
assertThat(userAgentHeaders).isNotNull().hasSize(1);
152+
153+
assertThat(userAgentHeaders.get(0)).doesNotMatch(METRIC_SEARCH_PATTERN.apply("J"));
154+
}
155+
156+
@Test
157+
void getObject_whenRegularS3Bucket_shouldNotIncludeS3ExpressFeatureIdInUserAgent() {
158+
GetObjectRequest getRequest = GetObjectRequest.builder()
159+
.bucket(REGULAR_BUCKET)
160+
.key(KEY)
161+
.build();
162+
163+
s3Client.getObject(getRequest, ResponseTransformer.toBytes());
164+
165+
SdkHttpRequest lastRequest = mockHttpClient.getLastRequest();
166+
assertThat(lastRequest).isNotNull();
167+
168+
List<String> userAgentHeaders = lastRequest.headers().get("User-Agent");
169+
assertThat(userAgentHeaders).isNotNull().hasSize(1);
170+
171+
assertThat(userAgentHeaders.get(0)).doesNotMatch(METRIC_SEARCH_PATTERN.apply("J"));
172+
}
173+
174+
}

0 commit comments

Comments
 (0)