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; + } +}