Skip to content

Commit edf8f9f

Browse files
committed
Adding client type and client name to user agent
1 parent 8777eb0 commit edf8f9f

File tree

11 files changed

+179
-14
lines changed

11 files changed

+179
-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: 36 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.
@@ -37,6 +40,9 @@ public class ApplyUserAgentStage implements MutableRequestToRequestPipeline {
3740
private static final String COMMA = ", ";
3841
private static final String SPACE = " ";
3942

43+
private static final String IO = "io";
44+
private static final String HTTP = "http";
45+
4046
private static final String AWS_EXECUTION_ENV_PREFIX = "exec-env/";
4147

4248
private static final String HEADER_USER_AGENT = "User-Agent";
@@ -70,6 +76,24 @@ private StringBuilder getUserAgent(SdkClientConfiguration config, List<ApiName>
7076
userAgent.append(SPACE).append(AWS_EXECUTION_ENV_PREFIX).append(awsExecutionEnvironment.trim());
7177
}
7278

79+
ClientType clientType = clientConfig.option(SdkClientOption.CLIENT_TYPE);
80+
81+
if (clientType == null) {
82+
clientType = ClientType.UNKNOWN;
83+
}
84+
85+
userAgent.append(SPACE)
86+
.append(IO)
87+
.append("/")
88+
.append(StringUtils.lowerCase(clientType.name()));
89+
90+
String clientName = clientName(clientType);
91+
92+
userAgent.append(SPACE)
93+
.append(HTTP)
94+
.append("/")
95+
.append(SdkHttpUtils.urlEncode(clientName));
96+
7397
if (!requestApiNames.isEmpty()) {
7498
String requestUserAgent = requestApiNames.stream()
7599
.map(n -> n.name() + "/" + n.version())
@@ -94,4 +118,16 @@ private String addUserAgentSuffix(StringBuilder userAgent, SdkClientConfiguratio
94118

95119
return userAgent.toString();
96120
}
121+
122+
private String clientName(ClientType clientType) {
123+
if (clientType.equals(ClientType.SYNC)) {
124+
return clientConfig.option(SdkClientOption.SYNC_HTTP_CLIENT).clientName();
125+
}
126+
127+
if (clientType.equals(ClientType.ASYNC)) {
128+
return clientConfig.option(SdkClientOption.ASYNC_HTTP_CLIENT).clientName();
129+
}
130+
131+
return ClientType.UNKNOWN.name();
132+
}
97133
}

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

Lines changed: 29 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,33 @@ 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("io/sync"));
180+
Assert.assertTrue(userAgent.contains("http/UNKNOWN"));
181+
}
182+
154183
@Test
155184
public void closeClient_shouldCloseDependencies() {
156185
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
@@ -69,6 +69,9 @@
6969
*/
7070
@SdkPublicApi
7171
public final class NettyNioAsyncHttpClient implements SdkAsyncHttpClient {
72+
73+
private static final String CLIENT_NAME = "NettyNio";
74+
7275
private static final Logger log = LoggerFactory.getLogger(NettyNioAsyncHttpClient.class);
7376
private static final long MAX_STREAMS_ALLOWED = 4294967295L; // unsigned 32-bit, 2^32 -1
7477

@@ -163,6 +166,11 @@ private void closeEventLoopUninterruptibly(EventLoopGroup eventLoopGroup) throws
163166
}
164167
}
165168

169+
@Override
170+
public String clientName() {
171+
return CLIENT_NAME;
172+
}
173+
166174
/**
167175
* Builder that allows configuration of the Netty NIO HTTP implementation. Use {@link #builder()} to configure and construct
168176
* a Netty HTTP client.

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

Lines changed: 17 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,13 @@ 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()).containsIgnoringCase("io/sync");
122+
assertThat(context.httpRequest().firstMatchingHeader("User-Agent").get()).containsIgnoringCase("http/UrlConnection");
123+
}
124+
}
111125
}

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()

0 commit comments

Comments
 (0)