Skip to content

Adding client type and client name to user agent #1218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 25, 2019
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
public enum ClientType {

ASYNC("Async"),
SYNC("Sync");
SYNC("Sync"),
UNKNOWN("Unknown");

private final String clientType;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,11 @@ public ExecutableHttpRequest prepareRequest(HttpExecuteRequest request) {
public void close() {
// Do nothing, this client is managed by the customer.
}

@Override
public String clientName() {
return delegate.clientName();
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,18 @@
import java.util.stream.Collectors;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.core.ApiName;
import software.amazon.awssdk.core.ClientType;
import software.amazon.awssdk.core.SdkSystemSetting;
import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption;
import software.amazon.awssdk.core.client.config.SdkClientConfiguration;
import software.amazon.awssdk.core.client.config.SdkClientOption;
import software.amazon.awssdk.core.internal.http.HttpClientDependencies;
import software.amazon.awssdk.core.internal.http.RequestExecutionContext;
import software.amazon.awssdk.core.internal.http.pipeline.MutableRequestToRequestPipeline;
import software.amazon.awssdk.core.internal.util.UserAgentUtils;
import software.amazon.awssdk.http.SdkHttpFullRequest;
import software.amazon.awssdk.utils.StringUtils;
import software.amazon.awssdk.utils.http.SdkHttpUtils;

/**
* Apply any custom user agent supplied, otherwise instrument the user agent with info about the SDK and environment.
Expand All @@ -37,6 +40,9 @@ public class ApplyUserAgentStage implements MutableRequestToRequestPipeline {
private static final String COMMA = ", ";
private static final String SPACE = " ";

private static final String IO = "io";
private static final String HTTP = "http";

private static final String AWS_EXECUTION_ENV_PREFIX = "exec-env/";

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

ClientType clientType = clientConfig.option(SdkClientOption.CLIENT_TYPE);

if (clientType == null) {
clientType = ClientType.UNKNOWN;
}

userAgent.append(SPACE)
.append(IO)
.append("/")
.append(StringUtils.lowerCase(clientType.name()));

String clientName = clientName(clientType);

userAgent.append(SPACE)
.append(HTTP)
.append("/")
.append(SdkHttpUtils.urlEncode(clientName));

if (!requestApiNames.isEmpty()) {
String requestUserAgent = requestApiNames.stream()
.map(n -> n.name() + "/" + n.version())
Expand All @@ -94,4 +118,16 @@ private String addUserAgentSuffix(StringBuilder userAgent, SdkClientConfiguratio

return userAgent.toString();
}

private String clientName(ClientType clientType) {
if (clientType.equals(ClientType.SYNC)) {
return clientConfig.option(SdkClientOption.SYNC_HTTP_CLIENT).clientName();
}

if (clientType.equals(ClientType.ASYNC)) {
return clientConfig.option(SdkClientOption.ASYNC_HTTP_CLIENT).clientName();
}

return ClientType.UNKNOWN.name();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import software.amazon.awssdk.core.ClientType;
import software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption;
import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption;
import software.amazon.awssdk.core.client.config.SdkClientConfiguration;
Expand Down Expand Up @@ -66,6 +67,7 @@ public void setUp() throws Exception {
BasicConfigurator.configure();
client = HttpTestUtils.testClientBuilder().httpClient(sdkHttpClient).build();
when(sdkHttpClient.prepareRequest(any())).thenReturn(abortableCallable);
when(sdkHttpClient.clientName()).thenReturn("UNKNOWN");
stubSuccessfulResponse();
}

Expand Down Expand Up @@ -151,6 +153,33 @@ public void testUserAgentPrefixAndSuffixAreAdded() {
Assert.assertTrue(userAgent.endsWith(suffix));
}

@Test
public void testUserAgentContainsHttpClientInfo() {
HttpResponseHandler<?> handler = mock(HttpResponseHandler.class);

SdkClientConfiguration config = HttpTestUtils.testClientConfiguration().toBuilder()
.option(SdkClientOption.SYNC_HTTP_CLIENT, sdkHttpClient)
.option(SdkClientOption.CLIENT_TYPE, ClientType.SYNC)
.option(SdkClientOption.ENDPOINT, URI.create("http://example.com"))
.build();
AmazonSyncHttpClient client = new AmazonSyncHttpClient(config);

client.requestExecutionBuilder()
.request(ValidSdkObjects.sdkHttpFullRequest().build())
.originalRequest(NoopTestRequest.builder().build())
.executionContext(ClientExecutionAndRequestTimerTestUtils.executionContext(null))
.execute(handler);

ArgumentCaptor<HttpExecuteRequest> httpRequestCaptor = ArgumentCaptor.forClass(HttpExecuteRequest.class);
verify(sdkHttpClient).prepareRequest(httpRequestCaptor.capture());

final String userAgent = httpRequestCaptor.getValue().httpRequest().firstMatchingHeader("User-Agent")
.orElseThrow(() -> new AssertionError("User-Agent header was not found"));

Assert.assertTrue(userAgent.contains("io/sync"));
Assert.assertTrue(userAgent.contains("http/UNKNOWN"));
}

@Test
public void closeClient_shouldCloseDependencies() {
SdkClientConfiguration config = HttpTestUtils.testClientConfiguration()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
@ThreadSafe
@SdkPublicApi
public interface SdkHttpClient extends SdkAutoCloseable {

/**
* Create a {@link ExecutableHttpRequest} that can be used to execute the HTTP request.
*
Expand All @@ -39,6 +40,21 @@ public interface SdkHttpClient extends SdkAutoCloseable {
*/
ExecutableHttpRequest prepareRequest(HttpExecuteRequest request);

/**
* Each HTTP client implementation should return a well-formed client name
* that allows requests to be identifiable back to the client that made the request.
* The client name should include the backing implementation as well as the Sync or Async
* to identify the transmission type of the request. Client names should only include
* alphanumeric characters. Examples of well formed client names include, ApacheSync, for
* requests using Apache's synchronous http client or NettyNioAsync for Netty's asynchronous
* http client.
*
* @return String containing the name of the client
*/
default String clientName() {
return "UNKNOWN";
}

/**
* Interface for creating an {@link SdkHttpClient} with service specific defaults applied.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@ public interface SdkAsyncHttpClient extends SdkAutoCloseable {
*/
CompletableFuture<Void> execute(AsyncExecuteRequest request);

/**
* Each HTTP client implementation should return a well-formed client name
* that allows requests to be identifiable back to the client that made the request.
* The client name should include the backing implementation as well as the Sync or Async
* to identify the transmission type of the request. Client names should only include
* alphanumeric characters. Examples of well formed client names include, Apache, for
* requests using Apache's http client or NettyNio for Netty's http client.
*
* @return String containing the name of the client
*/
default String clientName() {
return "UNKNOWN";
}

@FunctionalInterface
interface Builder<T extends SdkAsyncHttpClient.Builder<T>> extends SdkBuilder<T, SdkAsyncHttpClient> {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@
*/
@SdkPublicApi
public final class ApacheHttpClient implements SdkHttpClient {

public static final String CLIENT_NAME = "Apache";

private static final Logger log = Logger.loggerFor(ApacheHttpClient.class);

private final ApacheHttpRequestFactory apacheHttpRequestFactory = new ApacheHttpRequestFactory();
Expand Down Expand Up @@ -283,6 +286,11 @@ private ApacheHttpRequestConfig createRequestConfig(DefaultBuilder builder,
.build();
}

@Override
public String clientName() {
return CLIENT_NAME;
}

/**
* Builder for creating an instance of {@link SdkHttpClient}. The factory can be configured through the builder {@link
* #builder()}, once built it can create a {@link SdkHttpClient} via {@link #build()} or can be passed to the SDK
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@
*/
@SdkPublicApi
public final class NettyNioAsyncHttpClient implements SdkAsyncHttpClient {

private static final String CLIENT_NAME = "NettyNio";

private static final Logger log = LoggerFactory.getLogger(NettyNioAsyncHttpClient.class);
private static final long MAX_STREAMS_ALLOWED = 4294967295L; // unsigned 32-bit, 2^32 -1

Expand Down Expand Up @@ -163,6 +166,11 @@ private void closeEventLoopUninterruptibly(EventLoopGroup eventLoopGroup) throws
}
}

@Override
public String clientName() {
return CLIENT_NAME;
}

/**
* Builder that allows configuration of the Netty NIO HTTP implementation. Use {@link #builder()} to configure and construct
* a Netty HTTP client.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@

package software.amazon.awssdk.http.urlconnection;

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

import org.assertj.core.api.Assertions;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
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.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
Expand Down Expand Up @@ -53,6 +57,7 @@ public static void createResources() throws Exception {
.region(REGION)
.httpClient(UrlConnectionHttpClient.builder().build())
.credentialsProvider(CREDENTIALS_PROVIDER_CHAIN)
.overrideConfiguration(o -> o.addExecutionInterceptor(new UserAgentVerifyingInterceptor()))
.build();

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

@Test
public void verifyPutObject() {
Assertions.assertThat(objectCount(BUCKET_NAME)).isEqualTo(0);
assertThat(objectCount(BUCKET_NAME)).isEqualTo(0);

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


Assertions.assertThat(objectCount(BUCKET_NAME)).isEqualTo(1);
assertThat(objectCount(BUCKET_NAME)).isEqualTo(1);
}


Expand Down Expand Up @@ -108,4 +113,13 @@ private int objectCount(String bucket) {

return s3.listObjectsV2(listReq).keyCount();
}

private static final class UserAgentVerifyingInterceptor implements ExecutionInterceptor {

@Override
public void beforeTransmission(Context.BeforeTransmission context, ExecutionAttributes executionAttributes) {
assertThat(context.httpRequest().firstMatchingHeader("User-Agent").get()).containsIgnoringCase("io/sync");
assertThat(context.httpRequest().firstMatchingHeader("User-Agent").get()).containsIgnoringCase("http/UrlConnection");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@
@SdkPublicApi
public final class UrlConnectionHttpClient implements SdkHttpClient {

private static final String CLIENT_NAME = "UrlConnection";

private final AttributeMap options;
private final UrlConnectionFactory connectionFactory;

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

@Override
public String clientName() {
return CLIENT_NAME;
}

private HttpURLConnection createAndConfigureConnection(HttpExecuteRequest request) {
HttpURLConnection connection = connectionFactory.createConnection(request.httpRequest().getUri());
request.httpRequest()
Expand Down
Loading