diff --git a/http-client-spi/pom.xml b/http-client-spi/pom.xml
index 501cc03716f1..36b7cfbd526f 100644
--- a/http-client-spi/pom.xml
+++ b/http-client-spi/pom.xml
@@ -50,6 +50,11 @@
utils
${awsjavasdk.version}
+
+ software.amazon.awssdk
+ metrics-spi
+ ${awsjavasdk.version}
+
org.reactivestreams
reactive-streams
diff --git a/http-client-spi/src/main/java/software/amazon/awssdk/http/HttpExecuteRequest.java b/http-client-spi/src/main/java/software/amazon/awssdk/http/HttpExecuteRequest.java
index c814a4f5cebd..38b255a3acdd 100644
--- a/http-client-spi/src/main/java/software/amazon/awssdk/http/HttpExecuteRequest.java
+++ b/http-client-spi/src/main/java/software/amazon/awssdk/http/HttpExecuteRequest.java
@@ -17,6 +17,7 @@
import java.util.Optional;
import software.amazon.awssdk.annotations.SdkPublicApi;
+import software.amazon.awssdk.metrics.MetricCollector;
/**
* Request object containing the parameters necessary to make a synchronous HTTP request.
@@ -28,10 +29,12 @@ public final class HttpExecuteRequest {
private final SdkHttpRequest request;
private final Optional contentStreamProvider;
+ private final MetricCollector metricCollector;
private HttpExecuteRequest(BuilderImpl builder) {
this.request = builder.request;
this.contentStreamProvider = builder.contentStreamProvider;
+ this.metricCollector = builder.metricCollector;
}
/**
@@ -48,6 +51,13 @@ public Optional contentStreamProvider() {
return contentStreamProvider;
}
+ /**
+ * @return The {@link MetricCollector}.
+ */
+ public Optional metricCollector() {
+ return Optional.ofNullable(metricCollector);
+ }
+
public static Builder builder() {
return new BuilderImpl();
}
@@ -68,12 +78,22 @@ public interface Builder {
*/
Builder contentStreamProvider(ContentStreamProvider contentStreamProvider);
+ /**
+ * Set the {@link MetricCollector} to be used by the HTTP client to
+ * report metrics collected for this request.
+ *
+ * @param metricCollector The metric collector.
+ * @return This bilder for method chaining.
+ */
+ Builder metricCollector(MetricCollector metricCollector);
+
HttpExecuteRequest build();
}
private static class BuilderImpl implements Builder {
private SdkHttpRequest request;
private Optional contentStreamProvider = Optional.empty();
+ private MetricCollector metricCollector;
@Override
public Builder request(SdkHttpRequest request) {
@@ -87,6 +107,12 @@ public Builder contentStreamProvider(ContentStreamProvider contentStreamProvider
return this;
}
+ @Override
+ public Builder metricCollector(MetricCollector metricCollector) {
+ this.metricCollector = metricCollector;
+ return this;
+ }
+
@Override
public HttpExecuteRequest build() {
return new HttpExecuteRequest(this);
diff --git a/http-client-spi/src/main/java/software/amazon/awssdk/http/HttpMetric.java b/http-client-spi/src/main/java/software/amazon/awssdk/http/HttpMetric.java
new file mode 100644
index 000000000000..36cdfc6de305
--- /dev/null
+++ b/http-client-spi/src/main/java/software/amazon/awssdk/http/HttpMetric.java
@@ -0,0 +1,58 @@
+/*
+ * 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.http;
+
+import software.amazon.awssdk.annotations.SdkPublicApi;
+import software.amazon.awssdk.metrics.MetricCategory;
+import software.amazon.awssdk.metrics.SdkMetric;
+
+/**
+ * Metrics collected by HTTP clients.
+ */
+@SdkPublicApi
+public final class HttpMetric {
+ /**
+ * The name of the HTTP client.
+ */
+ public static final SdkMetric HTTP_CLIENT_NAME = metric("HttpClientName", String.class);
+
+ /**
+ * The maximum number of connections that will be pooled by the HTTP client.
+ */
+ public static final SdkMetric MAX_CONNECTIONS = metric("MaxConnections", Integer.class);
+
+ /**
+ * The number of idle connections in the connection pool that are ready to serve a request.
+ */
+ public static final SdkMetric AVAILABLE_CONNECTIONS = metric("AvailableConnections", Integer.class);
+
+ /**
+ * The number of connections from the connection pool that are busy serving requests.
+ */
+ public static final SdkMetric LEASED_CONNECTIONS = metric("LeasedConnections", Integer.class);
+
+ /**
+ * The number of requests awaiting a free connection from the pool.
+ */
+ public static final SdkMetric PENDING_CONNECTION_ACQUIRES = metric("PendingConnectionAcquires", Integer.class);
+
+ private HttpMetric() {
+ }
+
+ private static SdkMetric metric(String name, Class clzz) {
+ return SdkMetric.create(name, clzz, MetricCategory.DEFAULT, MetricCategory.HTTP_CLIENT);
+ }
+}
diff --git a/http-clients/apache-client/pom.xml b/http-clients/apache-client/pom.xml
index 94a8b10ace9f..df9b0394d1b5 100644
--- a/http-clients/apache-client/pom.xml
+++ b/http-clients/apache-client/pom.xml
@@ -33,6 +33,11 @@
http-client-spi
${awsjavasdk.version}
+
+ software.amazon.awssdk
+ metrics-spi
+ ${awsjavasdk.version}
+
software.amazon.awssdk
utils
diff --git a/http-clients/apache-client/src/main/java/software/amazon/awssdk/http/apache/ApacheHttpClient.java b/http-clients/apache-client/src/main/java/software/amazon/awssdk/http/apache/ApacheHttpClient.java
index 0367a86c6d80..a36ef4996297 100644
--- a/http-clients/apache-client/src/main/java/software/amazon/awssdk/http/apache/ApacheHttpClient.java
+++ b/http-clients/apache-client/src/main/java/software/amazon/awssdk/http/apache/ApacheHttpClient.java
@@ -18,6 +18,11 @@
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
+import static software.amazon.awssdk.http.HttpMetric.AVAILABLE_CONNECTIONS;
+import static software.amazon.awssdk.http.HttpMetric.HTTP_CLIENT_NAME;
+import static software.amazon.awssdk.http.HttpMetric.LEASED_CONNECTIONS;
+import static software.amazon.awssdk.http.HttpMetric.MAX_CONNECTIONS;
+import static software.amazon.awssdk.http.HttpMetric.PENDING_CONNECTION_ACQUIRES;
import static software.amazon.awssdk.utils.NumericUtils.saturatedCast;
import java.io.IOException;
@@ -57,6 +62,7 @@
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.DefaultSchemePortResolver;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.pool.PoolStats;
import org.apache.http.protocol.HttpRequestExecutor;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.annotations.SdkTestInternalApi;
@@ -72,6 +78,7 @@
import software.amazon.awssdk.http.TlsTrustManagersProvider;
import software.amazon.awssdk.http.apache.internal.ApacheHttpRequestConfig;
import software.amazon.awssdk.http.apache.internal.DefaultConfiguration;
+import software.amazon.awssdk.http.apache.internal.NoOpMetricCollector;
import software.amazon.awssdk.http.apache.internal.SdkProxyRoutePlanner;
import software.amazon.awssdk.http.apache.internal.conn.ClientConnectionManagerFactory;
import software.amazon.awssdk.http.apache.internal.conn.IdleConnectionReaper;
@@ -81,6 +88,7 @@
import software.amazon.awssdk.http.apache.internal.impl.ApacheSdkHttpClient;
import software.amazon.awssdk.http.apache.internal.impl.ConnectionManagerAwareHttpClient;
import software.amazon.awssdk.http.apache.internal.utils.ApacheUtils;
+import software.amazon.awssdk.metrics.MetricCollector;
import software.amazon.awssdk.utils.AttributeMap;
import software.amazon.awssdk.utils.Logger;
import software.amazon.awssdk.utils.Validate;
@@ -206,11 +214,15 @@ private boolean isProxyEnabled(ProxyConfiguration proxyConfiguration) {
@Override
public ExecutableHttpRequest prepareRequest(HttpExecuteRequest request) {
+ MetricCollector metricCollector = request.metricCollector().orElseGet(NoOpMetricCollector::create);
+ metricCollector.reportMetric(HTTP_CLIENT_NAME, clientName());
HttpRequestBase apacheRequest = toApacheRequest(request);
return new ExecutableHttpRequest() {
@Override
public HttpExecuteResponse call() throws IOException {
- return execute(apacheRequest);
+ HttpExecuteResponse executeResponse = execute(apacheRequest);
+ collectPoolMetric(metricCollector);
+ return executeResponse;
}
@Override
@@ -283,6 +295,18 @@ private ApacheHttpRequestConfig createRequestConfig(DefaultBuilder builder,
.build();
}
+ private void collectPoolMetric(MetricCollector metricCollector) {
+ HttpClientConnectionManager cm = httpClient.getHttpClientConnectionManager();
+ if (cm instanceof PoolingHttpClientConnectionManager) {
+ PoolingHttpClientConnectionManager poolingCm = (PoolingHttpClientConnectionManager) cm;
+ PoolStats totalStats = poolingCm.getTotalStats();
+ metricCollector.reportMetric(MAX_CONNECTIONS, totalStats.getMax());
+ metricCollector.reportMetric(AVAILABLE_CONNECTIONS, totalStats.getAvailable());
+ metricCollector.reportMetric(LEASED_CONNECTIONS, totalStats.getLeased());
+ metricCollector.reportMetric(PENDING_CONNECTION_ACQUIRES, totalStats.getPending());
+ }
+ }
+
@Override
public String clientName() {
return CLIENT_NAME;
diff --git a/http-clients/apache-client/src/main/java/software/amazon/awssdk/http/apache/internal/NoOpMetricCollector.java b/http-clients/apache-client/src/main/java/software/amazon/awssdk/http/apache/internal/NoOpMetricCollector.java
new file mode 100644
index 000000000000..a62894561350
--- /dev/null
+++ b/http-clients/apache-client/src/main/java/software/amazon/awssdk/http/apache/internal/NoOpMetricCollector.java
@@ -0,0 +1,52 @@
+/*
+ * 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.http.apache.internal;
+
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.metrics.MetricCollection;
+import software.amazon.awssdk.metrics.MetricCollector;
+import software.amazon.awssdk.metrics.SdkMetric;
+
+/**
+ * A metric collector that doesn't do anything.
+ */
+@SdkInternalApi
+public final class NoOpMetricCollector implements MetricCollector {
+ private static final NoOpMetricCollector INSTANCE = new NoOpMetricCollector();
+
+ @Override
+ public String name() {
+ return "NoOp";
+ }
+
+ @Override
+ public void reportMetric(SdkMetric metric, T data) {
+ }
+
+ @Override
+ public MetricCollector createChild(String name) {
+ throw new UnsupportedOperationException("No op collector does not support createChild");
+ }
+
+ @Override
+ public MetricCollection collect() {
+ throw new UnsupportedOperationException("No op collector does not support collect");
+ }
+
+ public static NoOpMetricCollector create() {
+ return INSTANCE;
+ }
+}
diff --git a/http-clients/apache-client/src/test/java/software/amazon/awssdk/http/apache/MetricReportingTest.java b/http-clients/apache-client/src/test/java/software/amazon/awssdk/http/apache/MetricReportingTest.java
new file mode 100644
index 000000000000..90132c6f2ea8
--- /dev/null
+++ b/http-clients/apache-client/src/test/java/software/amazon/awssdk/http/apache/MetricReportingTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.http.apache;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static software.amazon.awssdk.http.HttpMetric.AVAILABLE_CONNECTIONS;
+import static software.amazon.awssdk.http.HttpMetric.HTTP_CLIENT_NAME;
+import static software.amazon.awssdk.http.HttpMetric.LEASED_CONNECTIONS;
+import static software.amazon.awssdk.http.HttpMetric.MAX_CONNECTIONS;
+import static software.amazon.awssdk.http.HttpMetric.PENDING_CONNECTION_ACQUIRES;
+import java.io.IOException;
+import java.time.Duration;
+import org.apache.http.HttpVersion;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.conn.HttpClientConnectionManager;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.message.BasicHttpResponse;
+import org.apache.http.pool.PoolStats;
+import org.apache.http.protocol.HttpContext;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import software.amazon.awssdk.http.HttpExecuteRequest;
+import software.amazon.awssdk.http.SdkHttpFullRequest;
+import software.amazon.awssdk.http.SdkHttpMethod;
+import software.amazon.awssdk.http.apache.internal.ApacheHttpRequestConfig;
+import software.amazon.awssdk.http.apache.internal.impl.ConnectionManagerAwareHttpClient;
+import software.amazon.awssdk.metrics.MetricCollection;
+import software.amazon.awssdk.metrics.MetricCollector;
+import software.amazon.awssdk.utils.AttributeMap;
+
+@RunWith(MockitoJUnitRunner.class)
+public class MetricReportingTest {
+
+ @Mock
+ public ConnectionManagerAwareHttpClient mockHttpClient;
+
+ @Mock
+ public PoolingHttpClientConnectionManager cm;
+
+ @Before
+ public void methodSetup() throws IOException {
+ when(mockHttpClient.execute(any(HttpUriRequest.class), any(HttpContext.class)))
+ .thenReturn(new BasicHttpResponse(HttpVersion.HTTP_1_1, 200, "OK"));
+ when(mockHttpClient.getHttpClientConnectionManager()).thenReturn(cm);
+
+ PoolStats stats = new PoolStats(1, 2, 3, 4);
+ when(cm.getTotalStats()).thenReturn(stats);
+ }
+
+ @Test
+ public void prepareRequest_callableCalled_metricsReported() throws IOException {
+ ApacheHttpClient client = newClient();
+ MetricCollector collector = MetricCollector.create("test");
+ HttpExecuteRequest executeRequest = newRequest(collector);
+
+ client.prepareRequest(executeRequest).call();
+
+ MetricCollection collected = collector.collect();
+
+ assertThat(collected.metricValues(HTTP_CLIENT_NAME)).containsExactly("Apache");
+ assertThat(collected.metricValues(LEASED_CONNECTIONS)).containsExactly(1);
+ assertThat(collected.metricValues(PENDING_CONNECTION_ACQUIRES)).containsExactly(2);
+ assertThat(collected.metricValues(AVAILABLE_CONNECTIONS)).containsExactly(3);
+ assertThat(collected.metricValues(MAX_CONNECTIONS)).containsExactly(4);
+ }
+
+ @Test
+ public void prepareRequest_connectionManagerNotPooling_callableCalled_metricsReported() throws IOException {
+ ApacheHttpClient client = newClient();
+ when(mockHttpClient.getHttpClientConnectionManager()).thenReturn(mock(HttpClientConnectionManager.class));
+ MetricCollector collector = MetricCollector.create("test");
+ HttpExecuteRequest executeRequest = newRequest(collector);
+
+ client.prepareRequest(executeRequest).call();
+
+ MetricCollection collected = collector.collect();
+
+ assertThat(collected.metricValues(HTTP_CLIENT_NAME)).containsExactly("Apache");
+ assertThat(collected.metricValues(LEASED_CONNECTIONS)).isEmpty();
+ assertThat(collected.metricValues(PENDING_CONNECTION_ACQUIRES)).isEmpty();
+ assertThat(collected.metricValues(AVAILABLE_CONNECTIONS)).isEmpty();
+ assertThat(collected.metricValues(MAX_CONNECTIONS)).isEmpty();
+ }
+
+ private ApacheHttpClient newClient() {
+ ApacheHttpRequestConfig config = ApacheHttpRequestConfig.builder()
+ .connectionAcquireTimeout(Duration.ofDays(1))
+ .connectionTimeout(Duration.ofDays(1))
+ .socketTimeout(Duration.ofDays(1))
+ .proxyConfiguration(ProxyConfiguration.builder().build())
+ .build();
+
+ return new ApacheHttpClient(mockHttpClient, config, AttributeMap.empty());
+ }
+
+ private HttpExecuteRequest newRequest(MetricCollector collector) {
+ final SdkHttpFullRequest sdkRequest = SdkHttpFullRequest.builder()
+ .method(SdkHttpMethod.HEAD)
+ .host("amazonaws.com")
+ .protocol("https")
+ .build();
+
+ HttpExecuteRequest executeRequest = HttpExecuteRequest.builder()
+ .request(sdkRequest)
+ .metricCollector(collector)
+ .build();
+
+ return executeRequest;
+ }
+}