Skip to content

Commit f3bea93

Browse files
committed
Adding client type and client name to user agent
1 parent 64a1c78 commit f3bea93

File tree

11 files changed

+165
-14
lines changed

11 files changed

+165
-14
lines changed

core/sdk-core/src/main/java/software/amazon/awssdk/core/ClientType.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
public enum ClientType {
2525

2626
ASYNC("Async"),
27-
SYNC("Sync");
27+
SYNC("Sync"),
28+
UNKNOWN("Unknown");
2829

2930
private final String clientType;
3031

core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,11 @@ public ExecutableHttpRequest prepareRequest(HttpExecuteRequest request) {
390390
public void close() {
391391
// Do nothing, this client is managed by the customer.
392392
}
393+
394+
@Override
395+
public String clientName() {
396+
return delegate.clientName();
397+
}
393398
}
394399

395400
/**

core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApplyUserAgentStage.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,18 @@
1919
import java.util.stream.Collectors;
2020
import software.amazon.awssdk.annotations.SdkInternalApi;
2121
import software.amazon.awssdk.core.ApiName;
22+
import software.amazon.awssdk.core.ClientType;
2223
import software.amazon.awssdk.core.SdkSystemSetting;
2324
import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption;
2425
import software.amazon.awssdk.core.client.config.SdkClientConfiguration;
26+
import software.amazon.awssdk.core.client.config.SdkClientOption;
2527
import software.amazon.awssdk.core.internal.http.HttpClientDependencies;
2628
import software.amazon.awssdk.core.internal.http.RequestExecutionContext;
2729
import software.amazon.awssdk.core.internal.http.pipeline.MutableRequestToRequestPipeline;
2830
import software.amazon.awssdk.core.internal.util.UserAgentUtils;
2931
import software.amazon.awssdk.http.SdkHttpFullRequest;
3032
import software.amazon.awssdk.utils.StringUtils;
33+
import software.amazon.awssdk.utils.http.SdkHttpUtils;
3134

3235
/**
3336
* Apply any custom user agent supplied, otherwise instrument the user agent with info about the SDK and environment.
@@ -70,6 +73,19 @@ private StringBuilder getUserAgent(SdkClientConfiguration config, List<ApiName>
7073
userAgent.append(SPACE).append(AWS_EXECUTION_ENV_PREFIX).append(awsExecutionEnvironment.trim());
7174
}
7275

76+
ClientType clientType = clientConfig.option(SdkClientOption.CLIENT_TYPE);
77+
78+
if (clientType == null) {
79+
clientType = ClientType.UNKNOWN;
80+
}
81+
82+
String clientName = clientName(clientType);
83+
84+
userAgent.append(SPACE)
85+
.append(StringUtils.lowerCase(clientType.name()))
86+
.append("/")
87+
.append(SdkHttpUtils.urlEncode(clientName));
88+
7389
if (!requestApiNames.isEmpty()) {
7490
String requestUserAgent = requestApiNames.stream()
7591
.map(n -> n.name() + "/" + n.version())
@@ -94,4 +110,16 @@ private String addUserAgentSuffix(StringBuilder userAgent, SdkClientConfiguratio
94110

95111
return userAgent.toString();
96112
}
113+
114+
private String clientName(ClientType clientType) {
115+
if (clientType.equals(ClientType.SYNC)) {
116+
return clientConfig.option(SdkClientOption.SYNC_HTTP_CLIENT).clientName();
117+
}
118+
119+
if (clientType.equals(ClientType.ASYNC)) {
120+
return clientConfig.option(SdkClientOption.ASYNC_HTTP_CLIENT).clientName();
121+
}
122+
123+
return ClientType.UNKNOWN.name();
124+
}
97125
}

core/sdk-core/src/test/java/software/amazon/awssdk/core/http/AmazonHttpClientTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.mockito.ArgumentCaptor;
3333
import org.mockito.Mock;
3434
import org.mockito.runners.MockitoJUnitRunner;
35+
import software.amazon.awssdk.core.ClientType;
3536
import software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption;
3637
import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption;
3738
import software.amazon.awssdk.core.client.config.SdkClientConfiguration;
@@ -66,6 +67,7 @@ public void setUp() throws Exception {
6667
BasicConfigurator.configure();
6768
client = HttpTestUtils.testClientBuilder().httpClient(sdkHttpClient).build();
6869
when(sdkHttpClient.prepareRequest(any())).thenReturn(abortableCallable);
70+
when(sdkHttpClient.clientName()).thenReturn("UNKNOWN");
6971
stubSuccessfulResponse();
7072
}
7173

@@ -151,6 +153,32 @@ public void testUserAgentPrefixAndSuffixAreAdded() {
151153
Assert.assertTrue(userAgent.endsWith(suffix));
152154
}
153155

156+
@Test
157+
public void testUserAgentContainsHttpClientInfo() {
158+
HttpResponseHandler<?> handler = mock(HttpResponseHandler.class);
159+
160+
SdkClientConfiguration config = HttpTestUtils.testClientConfiguration().toBuilder()
161+
.option(SdkClientOption.SYNC_HTTP_CLIENT, sdkHttpClient)
162+
.option(SdkClientOption.CLIENT_TYPE, ClientType.SYNC)
163+
.option(SdkClientOption.ENDPOINT, URI.create("http://example.com"))
164+
.build();
165+
AmazonSyncHttpClient client = new AmazonSyncHttpClient(config);
166+
167+
client.requestExecutionBuilder()
168+
.request(ValidSdkObjects.sdkHttpFullRequest().build())
169+
.originalRequest(NoopTestRequest.builder().build())
170+
.executionContext(ClientExecutionAndRequestTimerTestUtils.executionContext(null))
171+
.execute(handler);
172+
173+
ArgumentCaptor<HttpExecuteRequest> httpRequestCaptor = ArgumentCaptor.forClass(HttpExecuteRequest.class);
174+
verify(sdkHttpClient).prepareRequest(httpRequestCaptor.capture());
175+
176+
final String userAgent = httpRequestCaptor.getValue().httpRequest().firstMatchingHeader("User-Agent")
177+
.orElseThrow(() -> new AssertionError("User-Agent header was not found"));
178+
179+
Assert.assertTrue(userAgent.contains("sync/UNKNOWN"));
180+
}
181+
154182
@Test
155183
public void closeClient_shouldCloseDependencies() {
156184
SdkClientConfiguration config = HttpTestUtils.testClientConfiguration()

http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpClient.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
@ThreadSafe
3232
@SdkPublicApi
3333
public interface SdkHttpClient extends SdkAutoCloseable {
34+
3435
/**
3536
* Create a {@link ExecutableHttpRequest} that can be used to execute the HTTP request.
3637
*
@@ -39,6 +40,21 @@ public interface SdkHttpClient extends SdkAutoCloseable {
3940
*/
4041
ExecutableHttpRequest prepareRequest(HttpExecuteRequest request);
4142

43+
/**
44+
* Each HTTP client implementation should return a well-formed client name
45+
* that allows requests to be identifiable back to the client that made the request.
46+
* The client name should include the backing implementation as well as the Sync or Async
47+
* to identify the transmission type of the request. Client names should only include
48+
* alphanumeric characters. Examples of well formed client names include, ApacheSync, for
49+
* requests using Apache's synchronous http client or NettyNioAsync for Netty's asynchronous
50+
* http client.
51+
*
52+
* @return String containing the name of the client
53+
*/
54+
default String clientName() {
55+
return "UNKNOWN";
56+
}
57+
4258
/**
4359
* Interface for creating an {@link SdkHttpClient} with service specific defaults applied.
4460
*/

http-client-spi/src/main/java/software/amazon/awssdk/http/async/SdkAsyncHttpClient.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,20 @@ public interface SdkAsyncHttpClient extends SdkAutoCloseable {
4444
*/
4545
CompletableFuture<Void> execute(AsyncExecuteRequest request);
4646

47+
/**
48+
* Each HTTP client implementation should return a well-formed client name
49+
* that allows requests to be identifiable back to the client that made the request.
50+
* The client name should include the backing implementation as well as the Sync or Async
51+
* to identify the transmission type of the request. Client names should only include
52+
* alphanumeric characters. Examples of well formed client names include, Apache, for
53+
* requests using Apache's http client or NettyNio for Netty's http client.
54+
*
55+
* @return String containing the name of the client
56+
*/
57+
default String clientName() {
58+
return "UNKNOWN";
59+
}
60+
4761
@FunctionalInterface
4862
interface Builder<T extends SdkAsyncHttpClient.Builder<T>> extends SdkBuilder<T, SdkAsyncHttpClient> {
4963
/**

http-clients/apache-client/src/main/java/software/amazon/awssdk/http/apache/ApacheHttpClient.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@
100100
*/
101101
@SdkPublicApi
102102
public final class ApacheHttpClient implements SdkHttpClient {
103+
104+
public static final String CLIENT_NAME = "Apache";
105+
103106
private static final Logger log = Logger.loggerFor(ApacheHttpClient.class);
104107

105108
private final ApacheHttpRequestFactory apacheHttpRequestFactory = new ApacheHttpRequestFactory();
@@ -283,6 +286,11 @@ private ApacheHttpRequestConfig createRequestConfig(DefaultBuilder builder,
283286
.build();
284287
}
285288

289+
@Override
290+
public String clientName() {
291+
return CLIENT_NAME;
292+
}
293+
286294
/**
287295
* Builder for creating an instance of {@link SdkHttpClient}. The factory can be configured through the builder {@link
288296
* #builder()}, once built it can create a {@link SdkHttpClient} via {@link #build()} or can be passed to the SDK

http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClient.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@
8383
*/
8484
@SdkPublicApi
8585
public final class NettyNioAsyncHttpClient implements SdkAsyncHttpClient {
86+
87+
private static final String CLIENT_NAME = "NettyNio";
88+
8689
private static final Logger log = LoggerFactory.getLogger(NettyNioAsyncHttpClient.class);
8790
private static final long MAX_STREAMS_ALLOWED = 4294967295L; // unsigned 32-bit, 2^32 -1
8891

@@ -254,6 +257,11 @@ private void closeEventLoopUninterruptibly(EventLoopGroup eventLoopGroup) throws
254257
}
255258
}
256259

260+
@Override
261+
public String clientName() {
262+
return CLIENT_NAME;
263+
}
264+
257265
/**
258266
* Builder that allows configuration of the Netty NIO HTTP implementation. Use {@link #builder()} to configure and construct
259267
* a Netty HTTP client.

http-clients/url-connection-client/src/it/java/software/amazon/awssdk/http/urlconnection/S3WithUrlHttpClientIntegrationTest.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@
1515

1616
package software.amazon.awssdk.http.urlconnection;
1717

18+
import static org.assertj.core.api.Assertions.assertThat;
1819
import static software.amazon.awssdk.testutils.service.AwsTestBase.CREDENTIALS_PROVIDER_CHAIN;
1920

20-
import org.assertj.core.api.Assertions;
2121
import org.junit.AfterClass;
2222
import org.junit.BeforeClass;
2323
import org.junit.Test;
24+
import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
25+
import software.amazon.awssdk.core.interceptor.Context;
26+
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
27+
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
2428
import software.amazon.awssdk.core.sync.RequestBody;
2529
import software.amazon.awssdk.regions.Region;
2630
import software.amazon.awssdk.services.s3.S3Client;
@@ -53,6 +57,7 @@ public static void createResources() throws Exception {
5357
.region(REGION)
5458
.httpClient(UrlConnectionHttpClient.builder().build())
5559
.credentialsProvider(CREDENTIALS_PROVIDER_CHAIN)
60+
.overrideConfiguration(o -> o.addExecutionInterceptor(new UserAgentVerifyingInterceptor()))
5661
.build();
5762

5863
createBucket(BUCKET_NAME, REGION);
@@ -69,14 +74,14 @@ public static void tearDown() {
6974

7075
@Test
7176
public void verifyPutObject() {
72-
Assertions.assertThat(objectCount(BUCKET_NAME)).isEqualTo(0);
77+
assertThat(objectCount(BUCKET_NAME)).isEqualTo(0);
7378

7479
// Put Object
7580
s3.putObject(PutObjectRequest.builder().bucket(BUCKET_NAME).key(KEY).build(),
7681
RequestBody.fromString("foobar"));
7782

7883

79-
Assertions.assertThat(objectCount(BUCKET_NAME)).isEqualTo(1);
84+
assertThat(objectCount(BUCKET_NAME)).isEqualTo(1);
8085
}
8186

8287

@@ -108,4 +113,12 @@ private int objectCount(String bucket) {
108113

109114
return s3.listObjectsV2(listReq).keyCount();
110115
}
116+
117+
private static final class UserAgentVerifyingInterceptor implements ExecutionInterceptor {
118+
119+
@Override
120+
public void beforeTransmission(Context.BeforeTransmission context, ExecutionAttributes executionAttributes) {
121+
assertThat(context.httpRequest().firstMatchingHeader("User-Agent").get()).contains("sync/UrlConnection");
122+
}
123+
}
111124
}

http-clients/url-connection-client/src/main/java/software/amazon/awssdk/http/urlconnection/UrlConnectionHttpClient.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@
6464
@SdkPublicApi
6565
public final class UrlConnectionHttpClient implements SdkHttpClient {
6666

67+
private static final String CLIENT_NAME = "UrlConnection";
68+
6769
private final AttributeMap options;
6870
private final UrlConnectionFactory connectionFactory;
6971

@@ -112,6 +114,11 @@ public void close() {
112114
// Nothing to close. The connections will be closed by closing the InputStreams.
113115
}
114116

117+
@Override
118+
public String clientName() {
119+
return CLIENT_NAME;
120+
}
121+
115122
private HttpURLConnection createAndConfigureConnection(HttpExecuteRequest request) {
116123
HttpURLConnection connection = connectionFactory.createConnection(request.httpRequest().getUri());
117124
request.httpRequest()

services/s3/src/it/java/software/amazon/awssdk/services/s3/S3IntegrationTestBase.java

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,19 @@
1515

1616
package software.amazon.awssdk.services.s3;
1717

18+
import static org.assertj.core.api.Assertions.assertThat;
19+
1820
import org.junit.BeforeClass;
21+
import software.amazon.awssdk.core.interceptor.Context;
22+
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
23+
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
1924
import software.amazon.awssdk.regions.Region;
2025
import software.amazon.awssdk.services.s3.model.BucketLocationConstraint;
2126
import software.amazon.awssdk.services.s3.model.CreateBucketConfiguration;
2227
import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
2328
import software.amazon.awssdk.services.s3.model.S3Exception;
24-
import software.amazon.awssdk.testutils.service.AwsTestBase;
2529
import software.amazon.awssdk.services.s3.utils.S3TestUtils;
30+
import software.amazon.awssdk.testutils.service.AwsTestBase;
2631

2732
/**
2833
* Base class for S3 integration tests. Loads AWS credentials from a properties
@@ -51,13 +56,17 @@ public static void setUp() throws Exception {
5156
protected static S3ClientBuilder s3ClientBuilder() {
5257
return S3Client.builder()
5358
.region(DEFAULT_REGION)
54-
.credentialsProvider(CREDENTIALS_PROVIDER_CHAIN);
59+
.credentialsProvider(CREDENTIALS_PROVIDER_CHAIN)
60+
.overrideConfiguration(o -> o.addExecutionInterceptor(
61+
new UserAgentVerifyingExecutionInterceptor("Apache")));
5562
}
5663

5764
protected static S3AsyncClientBuilder s3AsyncClientBuilder() {
5865
return S3AsyncClient.builder()
5966
.region(DEFAULT_REGION)
60-
.credentialsProvider(CREDENTIALS_PROVIDER_CHAIN);
67+
.credentialsProvider(CREDENTIALS_PROVIDER_CHAIN)
68+
.overrideConfiguration(o -> o.addExecutionInterceptor(
69+
new UserAgentVerifyingExecutionInterceptor("NettyNio")));
6170
}
6271

6372
protected static void createBucket(String bucketName) {
@@ -67,13 +76,13 @@ protected static void createBucket(String bucketName) {
6776
private static void createBucket(String bucketName, int retryCount) {
6877
try {
6978
s3.createBucket(
70-
CreateBucketRequest.builder()
71-
.bucket(bucketName)
72-
.createBucketConfiguration(
73-
CreateBucketConfiguration.builder()
74-
.locationConstraint(BucketLocationConstraint.US_WEST_2)
75-
.build())
76-
.build());
79+
CreateBucketRequest.builder()
80+
.bucket(bucketName)
81+
.createBucketConfiguration(
82+
CreateBucketConfiguration.builder()
83+
.locationConstraint(BucketLocationConstraint.US_WEST_2)
84+
.build())
85+
.build());
7786
} catch (S3Exception e) {
7887
System.err.println("Error attempting to create bucket: " + bucketName);
7988
if (e.awsErrorDetails().errorCode().equals("BucketAlreadyOwnedByYou")) {
@@ -96,4 +105,18 @@ private static void createBucket(String bucketName, int retryCount) {
96105
protected static void deleteBucketAndAllContents(String bucketName) {
97106
S3TestUtils.deleteBucketAndAllContents(s3, bucketName);
98107
}
108+
109+
private static class UserAgentVerifyingExecutionInterceptor implements ExecutionInterceptor {
110+
111+
private final String clientName;
112+
113+
public UserAgentVerifyingExecutionInterceptor(String clientName) {
114+
this.clientName = clientName;
115+
}
116+
117+
@Override
118+
public void beforeTransmission(Context.BeforeTransmission context, ExecutionAttributes executionAttributes) {
119+
assertThat(context.httpRequest().firstMatchingHeader("User-Agent").get()).contains("sync/" + clientName);
120+
}
121+
}
99122
}

0 commit comments

Comments
 (0)