diff --git a/metrics-httpclient5/pom.xml b/metrics-httpclient5/pom.xml
new file mode 100644
index 0000000000..b2cf6341c1
--- /dev/null
+++ b/metrics-httpclient5/pom.xml
@@ -0,0 +1,59 @@
+
+
+ 4.0.0
+
+
+ io.dropwizard.metrics
+ metrics-parent
+ 4.1.4-SNAPSHOT
+
+
+ metrics-httpclient5
+ Metrics Integration for Apache HttpClient 5.x
+ bundle
+
+ An Apache HttpClient 5.x wrapper providing Metrics instrumentation of connection pools, request
+ durations and rates, and other useful information.
+
+
+
+ com.codahale.metrics.httpclient
+ 5.0
+
+
+
+
+
+ io.dropwizard.metrics
+ metrics-bom
+ ${project.version}
+ pom
+ import
+
+
+
+
+
+
+ io.dropwizard.metrics
+ metrics-core
+
+
+ org.apache.httpcomponents.client5
+ httpclient5
+ ${http-client.version}
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ org.awaitility
+ awaitility
+ 4.0.2
+ test
+
+
+
diff --git a/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/HttpClientMetricNameStrategies.java b/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/HttpClientMetricNameStrategies.java
new file mode 100644
index 0000000000..a7911a2a23
--- /dev/null
+++ b/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/HttpClientMetricNameStrategies.java
@@ -0,0 +1,48 @@
+package com.codahale.metrics.httpclient5;
+
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.net.URIBuilder;
+
+import java.net.URISyntaxException;
+import java.util.Locale;
+
+import static com.codahale.metrics.MetricRegistry.name;
+
+public class HttpClientMetricNameStrategies {
+
+ public static final HttpClientMetricNameStrategy METHOD_ONLY =
+ (name, request) -> name(HttpClient.class,
+ name,
+ methodNameString(request));
+
+ public static final HttpClientMetricNameStrategy HOST_AND_METHOD =
+ (name, request) -> {
+ try {
+ return name(HttpClient.class,
+ name,
+ request.getUri().getHost(),
+ methodNameString(request));
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ };
+
+ public static final HttpClientMetricNameStrategy QUERYLESS_URL_AND_METHOD =
+ (name, request) -> {
+ try {
+ final URIBuilder url = new URIBuilder(request.getUri());
+ return name(HttpClient.class,
+ name,
+ url.removeQuery().build().toString(),
+ methodNameString(request));
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ };
+
+ private static String methodNameString(HttpRequest request) {
+ return request.getMethod().toLowerCase(Locale.ROOT) + "-requests";
+ }
+
+}
diff --git a/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/HttpClientMetricNameStrategy.java b/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/HttpClientMetricNameStrategy.java
new file mode 100644
index 0000000000..2077ef0cce
--- /dev/null
+++ b/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/HttpClientMetricNameStrategy.java
@@ -0,0 +1,17 @@
+package com.codahale.metrics.httpclient5;
+
+import com.codahale.metrics.MetricRegistry;
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.core5.http.HttpRequest;
+
+@FunctionalInterface
+public interface HttpClientMetricNameStrategy {
+
+ String getNameFor(String name, HttpRequest request);
+
+ default String getNameFor(String name, Exception exception) {
+ return MetricRegistry.name(HttpClient.class,
+ name,
+ exception.getClass().getSimpleName());
+ }
+}
diff --git a/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/InstrumentedAsyncClientConnectionManager.java b/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/InstrumentedAsyncClientConnectionManager.java
new file mode 100644
index 0000000000..ed4ffbb1a8
--- /dev/null
+++ b/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/InstrumentedAsyncClientConnectionManager.java
@@ -0,0 +1,164 @@
+package com.codahale.metrics.httpclient5;
+
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.MetricRegistry;
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.SchemePortResolver;
+import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
+import org.apache.hc.client5.http.io.HttpClientConnectionManager;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionManager;
+import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http.config.Lookup;
+import org.apache.hc.core5.http.config.Registry;
+import org.apache.hc.core5.http.config.RegistryBuilder;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
+import org.apache.hc.core5.pool.PoolReusePolicy;
+import org.apache.hc.core5.util.TimeValue;
+
+import static com.codahale.metrics.MetricRegistry.name;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A {@link HttpClientConnectionManager} which monitors the number of open connections.
+ */
+public class InstrumentedAsyncClientConnectionManager extends PoolingAsyncClientConnectionManager {
+ private static final String METRICS_PREFIX = AsyncClientConnectionManager.class.getName();
+
+ protected static Registry getDefaultTlsStrategy() {
+ return RegistryBuilder.create()
+ .register(URIScheme.HTTPS.id, DefaultClientTlsStrategy.getDefault())
+ .build();
+ }
+
+ private final MetricRegistry metricsRegistry;
+ private final String name;
+
+ InstrumentedAsyncClientConnectionManager(final MetricRegistry metricRegistry,
+ final String name,
+ final Lookup tlsStrategyLookup,
+ final PoolConcurrencyPolicy poolConcurrencyPolicy,
+ final PoolReusePolicy poolReusePolicy,
+ final TimeValue timeToLive,
+ final SchemePortResolver schemePortResolver,
+ final DnsResolver dnsResolver) {
+
+ super(tlsStrategyLookup, poolConcurrencyPolicy, poolReusePolicy, timeToLive, schemePortResolver, dnsResolver);
+ this.metricsRegistry = requireNonNull(metricRegistry, "metricRegistry");
+ this.name = name;
+
+ metricRegistry.register(name(METRICS_PREFIX, name, "available-connections"),
+ (Gauge) () -> {
+ // this acquires a lock on the connection pool; remove if contention sucks
+ return getTotalStats().getAvailable();
+ });
+ metricRegistry.register(name(METRICS_PREFIX, name, "leased-connections"),
+ (Gauge) () -> {
+ // this acquires a lock on the connection pool; remove if contention sucks
+ return getTotalStats().getLeased();
+ });
+ metricRegistry.register(name(METRICS_PREFIX, name, "max-connections"),
+ (Gauge) () -> {
+ // this acquires a lock on the connection pool; remove if contention sucks
+ return getTotalStats().getMax();
+ });
+ metricRegistry.register(name(METRICS_PREFIX, name, "pending-connections"),
+ (Gauge) () -> {
+ // this acquires a lock on the connection pool; remove if contention sucks
+ return getTotalStats().getPending();
+ });
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void close() {
+ close(CloseMode.GRACEFUL);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void close(CloseMode closeMode) {
+ super.close(closeMode);
+ metricsRegistry.remove(name(METRICS_PREFIX, name, "available-connections"));
+ metricsRegistry.remove(name(METRICS_PREFIX, name, "leased-connections"));
+ metricsRegistry.remove(name(METRICS_PREFIX, name, "max-connections"));
+ metricsRegistry.remove(name(METRICS_PREFIX, name, "pending-connections"));
+ }
+
+ public static Builder builder(MetricRegistry metricsRegistry) {
+ return new Builder().metricsRegistry(metricsRegistry);
+ }
+
+ public static class Builder {
+ private MetricRegistry metricsRegistry;
+ private String name;
+ private Lookup tlsStrategyLookup = getDefaultTlsStrategy();
+ private SchemePortResolver schemePortResolver;
+ private DnsResolver dnsResolver;
+ private PoolConcurrencyPolicy poolConcurrencyPolicy;
+ private PoolReusePolicy poolReusePolicy;
+ private TimeValue timeToLive = TimeValue.NEG_ONE_MILLISECOND;
+
+ Builder() {
+ }
+
+ public Builder metricsRegistry(MetricRegistry metricRegistry) {
+ this.metricsRegistry = requireNonNull(metricRegistry, "metricRegistry");
+ return this;
+ }
+
+ public Builder name(final String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder schemePortResolver(SchemePortResolver schemePortResolver) {
+ this.schemePortResolver = schemePortResolver;
+ return this;
+ }
+
+ public Builder dnsResolver(DnsResolver dnsResolver) {
+ this.dnsResolver = dnsResolver;
+ return this;
+ }
+
+ public Builder timeToLive(TimeValue timeToLive) {
+ this.timeToLive = timeToLive;
+ return this;
+ }
+
+ public Builder tlsStrategyLookup(Lookup tlsStrategyLookup) {
+ this.tlsStrategyLookup = tlsStrategyLookup;
+ return this;
+ }
+
+ public Builder poolConcurrencyPolicy(PoolConcurrencyPolicy poolConcurrencyPolicy) {
+ this.poolConcurrencyPolicy = poolConcurrencyPolicy;
+ return this;
+ }
+
+ public Builder poolReusePolicy(PoolReusePolicy poolReusePolicy) {
+ this.poolReusePolicy = poolReusePolicy;
+ return this;
+ }
+
+ public InstrumentedAsyncClientConnectionManager build() {
+ return new InstrumentedAsyncClientConnectionManager(
+ metricsRegistry,
+ name,
+ tlsStrategyLookup,
+ poolConcurrencyPolicy,
+ poolReusePolicy,
+ timeToLive,
+ schemePortResolver,
+ dnsResolver);
+ }
+ }
+
+}
diff --git a/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/InstrumentedAsyncExecChainHandler.java b/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/InstrumentedAsyncExecChainHandler.java
new file mode 100644
index 0000000000..f99b22888a
--- /dev/null
+++ b/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/InstrumentedAsyncExecChainHandler.java
@@ -0,0 +1,99 @@
+package com.codahale.metrics.httpclient5;
+
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.Timer;
+import org.apache.hc.client5.http.async.AsyncExecCallback;
+import org.apache.hc.client5.http.async.AsyncExecChain;
+import org.apache.hc.client5.http.async.AsyncExecChainHandler;
+import org.apache.hc.core5.http.EntityDetails;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.nio.AsyncDataConsumer;
+import org.apache.hc.core5.http.nio.AsyncEntityProducer;
+
+import java.io.IOException;
+
+import static java.util.Objects.requireNonNull;
+
+class InstrumentedAsyncExecChainHandler implements AsyncExecChainHandler {
+ private final MetricRegistry registry;
+ private final HttpClientMetricNameStrategy metricNameStrategy;
+ private final String name;
+
+ public InstrumentedAsyncExecChainHandler(MetricRegistry registry, HttpClientMetricNameStrategy metricNameStrategy) {
+ this(registry, metricNameStrategy, null);
+ }
+
+ public InstrumentedAsyncExecChainHandler(MetricRegistry registry,
+ HttpClientMetricNameStrategy metricNameStrategy,
+ String name) {
+ this.registry = requireNonNull(registry, "registry");
+ this.metricNameStrategy = requireNonNull(metricNameStrategy, "metricNameStrategy");
+ this.name = name;
+ }
+
+ @Override
+ public void execute(HttpRequest request,
+ AsyncEntityProducer entityProducer,
+ AsyncExecChain.Scope scope,
+ AsyncExecChain chain,
+ AsyncExecCallback asyncExecCallback) throws HttpException, IOException {
+ final InstrumentedAsyncExecCallback instrumentedAsyncExecCallback =
+ new InstrumentedAsyncExecCallback(registry, metricNameStrategy, name, asyncExecCallback, request);
+ chain.proceed(request, entityProducer, scope, instrumentedAsyncExecCallback);
+
+ }
+
+ final static class InstrumentedAsyncExecCallback implements AsyncExecCallback {
+ private final MetricRegistry registry;
+ private final HttpClientMetricNameStrategy metricNameStrategy;
+ private final String name;
+ private final AsyncExecCallback delegate;
+ private final Timer.Context timerContext;
+
+ public InstrumentedAsyncExecCallback(MetricRegistry registry,
+ HttpClientMetricNameStrategy metricNameStrategy,
+ String name,
+ AsyncExecCallback delegate,
+ HttpRequest request) {
+ this.registry = registry;
+ this.metricNameStrategy = metricNameStrategy;
+ this.name = name;
+ this.delegate = delegate;
+ this.timerContext = timer(request).time();
+ }
+
+ @Override
+ public AsyncDataConsumer handleResponse(HttpResponse response, EntityDetails entityDetails) throws HttpException, IOException {
+ return delegate.handleResponse(response, entityDetails);
+ }
+
+ @Override
+ public void handleInformationResponse(HttpResponse response) throws HttpException, IOException {
+ delegate.handleInformationResponse(response);
+ }
+
+ @Override
+ public void completed() {
+ delegate.completed();
+ timerContext.stop();
+ }
+
+ @Override
+ public void failed(Exception cause) {
+ delegate.failed(cause);
+ meter(cause).mark();
+ timerContext.stop();
+ }
+
+ private Timer timer(HttpRequest request) {
+ return registry.timer(metricNameStrategy.getNameFor(name, request));
+ }
+
+ private Meter meter(Exception e) {
+ return registry.meter(metricNameStrategy.getNameFor(name, e));
+ }
+ }
+}
diff --git a/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/InstrumentedHttpAsyncClients.java b/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/InstrumentedHttpAsyncClients.java
new file mode 100644
index 0000000000..5ef3b0643c
--- /dev/null
+++ b/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/InstrumentedHttpAsyncClients.java
@@ -0,0 +1,35 @@
+package com.codahale.metrics.httpclient5;
+
+import com.codahale.metrics.MetricRegistry;
+import org.apache.hc.client5.http.impl.ChainElement;
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
+
+import static com.codahale.metrics.httpclient5.HttpClientMetricNameStrategies.METHOD_ONLY;
+
+public class InstrumentedHttpAsyncClients {
+ private InstrumentedHttpAsyncClients() {
+ super();
+ }
+
+ public static CloseableHttpAsyncClient createDefault(MetricRegistry metricRegistry) {
+ return createDefault(metricRegistry, METHOD_ONLY);
+ }
+
+ public static CloseableHttpAsyncClient createDefault(MetricRegistry metricRegistry,
+ HttpClientMetricNameStrategy metricNameStrategy) {
+ return custom(metricRegistry, metricNameStrategy).build();
+ }
+
+ public static HttpAsyncClientBuilder custom(MetricRegistry metricRegistry) {
+ return custom(metricRegistry, METHOD_ONLY);
+ }
+
+ public static HttpAsyncClientBuilder custom(MetricRegistry metricRegistry,
+ HttpClientMetricNameStrategy metricNameStrategy) {
+ return HttpAsyncClientBuilder.create()
+ .setConnectionManager(InstrumentedAsyncClientConnectionManager.builder(metricRegistry).build())
+ .addExecInterceptorBefore(ChainElement.CONNECT.name(), "dropwizard-metrics", new InstrumentedAsyncExecChainHandler(metricRegistry, metricNameStrategy));
+ }
+
+}
diff --git a/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/InstrumentedHttpClientConnectionManager.java b/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/InstrumentedHttpClientConnectionManager.java
new file mode 100644
index 0000000000..ff3a421d43
--- /dev/null
+++ b/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/InstrumentedHttpClientConnectionManager.java
@@ -0,0 +1,184 @@
+package com.codahale.metrics.httpclient5;
+
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.MetricRegistry;
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.SchemePortResolver;
+import org.apache.hc.client5.http.impl.io.DefaultHttpClientConnectionOperator;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.apache.hc.client5.http.io.HttpClientConnectionManager;
+import org.apache.hc.client5.http.io.HttpClientConnectionOperator;
+import org.apache.hc.client5.http.io.ManagedHttpClientConnection;
+import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
+import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
+import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http.config.Registry;
+import org.apache.hc.core5.http.config.RegistryBuilder;
+import org.apache.hc.core5.http.io.HttpConnectionFactory;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
+import org.apache.hc.core5.pool.PoolReusePolicy;
+import org.apache.hc.core5.util.TimeValue;
+
+import static com.codahale.metrics.MetricRegistry.name;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A {@link HttpClientConnectionManager} which monitors the number of open connections.
+ */
+public class InstrumentedHttpClientConnectionManager extends PoolingHttpClientConnectionManager {
+ private static final String METRICS_PREFIX = HttpClientConnectionManager.class.getName();
+
+ protected static Registry getDefaultRegistry() {
+ return RegistryBuilder.create()
+ .register(URIScheme.HTTP.id, PlainConnectionSocketFactory.getSocketFactory())
+ .register(URIScheme.HTTPS.id, SSLConnectionSocketFactory.getSocketFactory())
+ .build();
+ }
+
+ private final MetricRegistry metricsRegistry;
+ private final String name;
+
+ InstrumentedHttpClientConnectionManager(final MetricRegistry metricRegistry,
+ final String name,
+ final HttpClientConnectionOperator httpClientConnectionOperator,
+ final PoolConcurrencyPolicy poolConcurrencyPolicy,
+ final PoolReusePolicy poolReusePolicy,
+ final TimeValue timeToLive,
+ final HttpConnectionFactory connFactory) {
+
+ super(httpClientConnectionOperator, poolConcurrencyPolicy, poolReusePolicy, timeToLive, connFactory);
+ this.metricsRegistry = requireNonNull(metricRegistry, "metricRegistry");
+ this.name = name;
+
+ metricRegistry.register(name(METRICS_PREFIX, name, "available-connections"),
+ (Gauge) () -> {
+ // this acquires a lock on the connection pool; remove if contention sucks
+ return getTotalStats().getAvailable();
+ });
+ metricRegistry.register(name(METRICS_PREFIX, name, "leased-connections"),
+ (Gauge) () -> {
+ // this acquires a lock on the connection pool; remove if contention sucks
+ return getTotalStats().getLeased();
+ });
+ metricRegistry.register(name(METRICS_PREFIX, name, "max-connections"),
+ (Gauge) () -> {
+ // this acquires a lock on the connection pool; remove if contention sucks
+ return getTotalStats().getMax();
+ });
+ metricRegistry.register(name(METRICS_PREFIX, name, "pending-connections"),
+ (Gauge) () -> {
+ // this acquires a lock on the connection pool; remove if contention sucks
+ return getTotalStats().getPending();
+ });
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void close() {
+ close(CloseMode.GRACEFUL);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void close(CloseMode closeMode) {
+ super.close(closeMode);
+ metricsRegistry.remove(name(METRICS_PREFIX, name, "available-connections"));
+ metricsRegistry.remove(name(METRICS_PREFIX, name, "leased-connections"));
+ metricsRegistry.remove(name(METRICS_PREFIX, name, "max-connections"));
+ metricsRegistry.remove(name(METRICS_PREFIX, name, "pending-connections"));
+ }
+
+ public static Builder builder(MetricRegistry metricsRegistry) {
+ return new Builder().metricsRegistry(metricsRegistry);
+ }
+
+ public static class Builder {
+ private MetricRegistry metricsRegistry;
+ private String name;
+ private HttpClientConnectionOperator httpClientConnectionOperator;
+ private Registry socketFactoryRegistry = getDefaultRegistry();
+ private SchemePortResolver schemePortResolver;
+ private DnsResolver dnsResolver;
+ private PoolConcurrencyPolicy poolConcurrencyPolicy;
+ private PoolReusePolicy poolReusePolicy;
+ private TimeValue timeToLive = TimeValue.NEG_ONE_MILLISECOND;
+ private HttpConnectionFactory connFactory;
+
+ Builder() {
+ }
+
+ public Builder metricsRegistry(MetricRegistry metricRegistry) {
+ this.metricsRegistry = requireNonNull(metricRegistry, "metricRegistry");
+ return this;
+ }
+
+ public Builder name(final String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder socketFactoryRegistry(Registry socketFactoryRegistry) {
+ this.socketFactoryRegistry = requireNonNull(socketFactoryRegistry, "socketFactoryRegistry");
+ return this;
+ }
+
+ public Builder connFactory(HttpConnectionFactory connFactory) {
+ this.connFactory = connFactory;
+ return this;
+ }
+
+ public Builder schemePortResolver(SchemePortResolver schemePortResolver) {
+ this.schemePortResolver = schemePortResolver;
+ return this;
+ }
+
+ public Builder dnsResolver(DnsResolver dnsResolver) {
+ this.dnsResolver = dnsResolver;
+ return this;
+ }
+
+ public Builder timeToLive(TimeValue timeToLive) {
+ this.timeToLive = timeToLive;
+ return this;
+ }
+
+ public Builder httpClientConnectionOperator(HttpClientConnectionOperator httpClientConnectionOperator) {
+ this.httpClientConnectionOperator = httpClientConnectionOperator;
+ return this;
+ }
+
+ public Builder poolConcurrencyPolicy(PoolConcurrencyPolicy poolConcurrencyPolicy) {
+ this.poolConcurrencyPolicy = poolConcurrencyPolicy;
+ return this;
+ }
+
+ public Builder poolReusePolicy(PoolReusePolicy poolReusePolicy) {
+ this.poolReusePolicy = poolReusePolicy;
+ return this;
+ }
+
+ public InstrumentedHttpClientConnectionManager build() {
+ if (httpClientConnectionOperator == null) {
+ httpClientConnectionOperator = new DefaultHttpClientConnectionOperator(
+ socketFactoryRegistry,
+ schemePortResolver,
+ dnsResolver);
+ }
+
+ return new InstrumentedHttpClientConnectionManager(
+ metricsRegistry,
+ name,
+ httpClientConnectionOperator,
+ poolConcurrencyPolicy,
+ poolReusePolicy,
+ timeToLive,
+ connFactory);
+ }
+ }
+}
diff --git a/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/InstrumentedHttpClients.java b/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/InstrumentedHttpClients.java
new file mode 100644
index 0000000000..f8f90f270f
--- /dev/null
+++ b/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/InstrumentedHttpClients.java
@@ -0,0 +1,34 @@
+package com.codahale.metrics.httpclient5;
+
+import com.codahale.metrics.MetricRegistry;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
+
+import static com.codahale.metrics.httpclient5.HttpClientMetricNameStrategies.METHOD_ONLY;
+
+public class InstrumentedHttpClients {
+ private InstrumentedHttpClients() {
+ super();
+ }
+
+ public static CloseableHttpClient createDefault(MetricRegistry metricRegistry) {
+ return createDefault(metricRegistry, METHOD_ONLY);
+ }
+
+ public static CloseableHttpClient createDefault(MetricRegistry metricRegistry,
+ HttpClientMetricNameStrategy metricNameStrategy) {
+ return custom(metricRegistry, metricNameStrategy).build();
+ }
+
+ public static HttpClientBuilder custom(MetricRegistry metricRegistry) {
+ return custom(metricRegistry, METHOD_ONLY);
+ }
+
+ public static HttpClientBuilder custom(MetricRegistry metricRegistry,
+ HttpClientMetricNameStrategy metricNameStrategy) {
+ return HttpClientBuilder.create()
+ .setRequestExecutor(new InstrumentedHttpRequestExecutor(metricRegistry, metricNameStrategy))
+ .setConnectionManager(InstrumentedHttpClientConnectionManager.builder(metricRegistry).build());
+ }
+
+}
diff --git a/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/InstrumentedHttpRequestExecutor.java b/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/InstrumentedHttpRequestExecutor.java
new file mode 100644
index 0000000000..5ffc465f4a
--- /dev/null
+++ b/metrics-httpclient5/src/main/java/com/codahale/metrics/httpclient5/InstrumentedHttpRequestExecutor.java
@@ -0,0 +1,78 @@
+package com.codahale.metrics.httpclient5;
+
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.Timer;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ConnectionReuseStrategy;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.impl.Http1StreamListener;
+import org.apache.hc.core5.http.impl.io.HttpRequestExecutor;
+import org.apache.hc.core5.http.io.HttpClientConnection;
+import org.apache.hc.core5.http.io.HttpResponseInformationCallback;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.util.Timeout;
+
+import java.io.IOException;
+
+public class InstrumentedHttpRequestExecutor extends HttpRequestExecutor {
+ private final MetricRegistry registry;
+ private final HttpClientMetricNameStrategy metricNameStrategy;
+ private final String name;
+
+ public InstrumentedHttpRequestExecutor(MetricRegistry registry,
+ HttpClientMetricNameStrategy metricNameStrategy) {
+ this(registry, metricNameStrategy, null);
+ }
+
+ public InstrumentedHttpRequestExecutor(MetricRegistry registry,
+ HttpClientMetricNameStrategy metricNameStrategy,
+ String name) {
+ this(registry, metricNameStrategy, name, HttpRequestExecutor.DEFAULT_WAIT_FOR_CONTINUE);
+ }
+
+ public InstrumentedHttpRequestExecutor(MetricRegistry registry,
+ HttpClientMetricNameStrategy metricNameStrategy,
+ String name,
+ Timeout waitForContinue) {
+ this(registry, metricNameStrategy, name, waitForContinue, null, null);
+ }
+
+ public InstrumentedHttpRequestExecutor(MetricRegistry registry,
+ HttpClientMetricNameStrategy metricNameStrategy,
+ String name,
+ Timeout waitForContinue,
+ ConnectionReuseStrategy connReuseStrategy,
+ Http1StreamListener streamListener) {
+ super(waitForContinue, connReuseStrategy, streamListener);
+ this.registry = registry;
+ this.name = name;
+ this.metricNameStrategy = metricNameStrategy;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ClassicHttpResponse execute(ClassicHttpRequest request, HttpClientConnection conn, HttpResponseInformationCallback informationCallback, HttpContext context) throws IOException, HttpException {
+ final Timer.Context timerContext = timer(request).time();
+ try {
+ return super.execute(request, conn, informationCallback, context);
+ } catch (HttpException | IOException e) {
+ meter(e).mark();
+ throw e;
+ } finally {
+ timerContext.stop();
+ }
+ }
+
+ private Timer timer(HttpRequest request) {
+ return registry.timer(metricNameStrategy.getNameFor(name, request));
+ }
+
+ private Meter meter(Exception e) {
+ return registry.meter(metricNameStrategy.getNameFor(name, e));
+ }
+}
diff --git a/metrics-httpclient5/src/test/java/com/codahale/metrics/httpclient5/HttpClientMetricNameStrategiesTest.java b/metrics-httpclient5/src/test/java/com/codahale/metrics/httpclient5/HttpClientMetricNameStrategiesTest.java
new file mode 100644
index 0000000000..b1e88382a4
--- /dev/null
+++ b/metrics-httpclient5/src/test/java/com/codahale/metrics/httpclient5/HttpClientMetricNameStrategiesTest.java
@@ -0,0 +1,86 @@
+package com.codahale.metrics.httpclient5;
+
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+import org.apache.hc.client5.http.classic.methods.HttpPut;
+import org.apache.hc.client5.http.utils.URIUtils;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.message.HttpRequestWrapper;
+import org.junit.Test;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import static com.codahale.metrics.httpclient5.HttpClientMetricNameStrategies.HOST_AND_METHOD;
+import static com.codahale.metrics.httpclient5.HttpClientMetricNameStrategies.METHOD_ONLY;
+import static com.codahale.metrics.httpclient5.HttpClientMetricNameStrategies.QUERYLESS_URL_AND_METHOD;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+public class HttpClientMetricNameStrategiesTest {
+
+ @Test
+ public void methodOnlyWithName() {
+ assertThat(METHOD_ONLY.getNameFor("some-service", new HttpGet("/whatever")),
+ is("org.apache.hc.client5.http.classic.HttpClient.some-service.get-requests"));
+ }
+
+ @Test
+ public void methodOnlyWithoutName() {
+ assertThat(METHOD_ONLY.getNameFor(null, new HttpGet("/whatever")),
+ is("org.apache.hc.client5.http.classic.HttpClient.get-requests"));
+ }
+
+ @Test
+ public void hostAndMethodWithName() {
+ assertThat(HOST_AND_METHOD.getNameFor("some-service", new HttpPost("http://my.host.com/whatever")),
+ is("org.apache.hc.client5.http.classic.HttpClient.some-service.my.host.com.post-requests"));
+ }
+
+ @Test
+ public void hostAndMethodWithoutName() {
+ assertThat(HOST_AND_METHOD.getNameFor(null, new HttpPost("http://my.host.com/whatever")),
+ is("org.apache.hc.client5.http.classic.HttpClient.my.host.com.post-requests"));
+ }
+
+ @Test
+ public void hostAndMethodWithNameInWrappedRequest() throws URISyntaxException {
+ HttpRequest request = rewriteRequestURI(new HttpPost("http://my.host.com/whatever"));
+
+ assertThat(HOST_AND_METHOD.getNameFor("some-service", request),
+ is("org.apache.hc.client5.http.classic.HttpClient.some-service.my.host.com.post-requests"));
+ }
+
+ @Test
+ public void hostAndMethodWithoutNameInWrappedRequest() throws URISyntaxException {
+ HttpRequest request = rewriteRequestURI(new HttpPost("http://my.host.com/whatever"));
+
+ assertThat(HOST_AND_METHOD.getNameFor(null, request),
+ is("org.apache.hc.client5.http.classic.HttpClient.my.host.com.post-requests"));
+ }
+
+ @Test
+ public void querylessUrlAndMethodWithName() {
+ assertThat(QUERYLESS_URL_AND_METHOD.getNameFor(
+ "some-service",
+ new HttpPut("https://thing.com:8090/my/path?ignore=this&and=this")),
+ is("org.apache.hc.client5.http.classic.HttpClient.some-service.https://thing.com:8090/my/path.put-requests"));
+ }
+
+ @Test
+ public void querylessUrlAndMethodWithNameInWrappedRequest() throws URISyntaxException {
+ HttpRequest request = rewriteRequestURI(new HttpPut("https://thing.com:8090/my/path?ignore=this&and=this"));
+ assertThat(QUERYLESS_URL_AND_METHOD.getNameFor(
+ "some-service",
+ request),
+ is("org.apache.hc.client5.http.classic.HttpClient.some-service.https://thing.com:8090/my/path.put-requests"));
+ }
+
+ private static HttpRequest rewriteRequestURI(HttpRequest request) throws URISyntaxException {
+ HttpRequestWrapper wrapper = new HttpRequestWrapper(request);
+ URI uri = URIUtils.rewriteURI(wrapper.getUri(), null, true);
+ wrapper.setUri(uri);
+
+ return wrapper;
+ }
+}
diff --git a/metrics-httpclient5/src/test/java/com/codahale/metrics/httpclient5/InstrumentedAsyncClientConnectionManagerTest.java b/metrics-httpclient5/src/test/java/com/codahale/metrics/httpclient5/InstrumentedAsyncClientConnectionManagerTest.java
new file mode 100644
index 0000000000..9a3cc14fd9
--- /dev/null
+++ b/metrics-httpclient5/src/test/java/com/codahale/metrics/httpclient5/InstrumentedAsyncClientConnectionManagerTest.java
@@ -0,0 +1,41 @@
+package com.codahale.metrics.httpclient5;
+
+import com.codahale.metrics.MetricRegistry;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import static junit.framework.TestCase.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+
+public class InstrumentedAsyncClientConnectionManagerTest {
+ private final MetricRegistry metricRegistry = new MetricRegistry();
+
+ @Test
+ public void shouldRemoveGauges() {
+ final InstrumentedAsyncClientConnectionManager instrumentedHttpClientConnectionManager = InstrumentedAsyncClientConnectionManager.builder(metricRegistry).build();
+ Assert.assertEquals(4, metricRegistry.getGauges().size());
+
+ instrumentedHttpClientConnectionManager.close();
+ Assert.assertEquals(0, metricRegistry.getGauges().size());
+
+ // should be able to create another one with the same name ("")
+ InstrumentedHttpClientConnectionManager.builder(metricRegistry).build().close();
+ }
+
+ @Test
+ public void configurableViaBuilder() {
+ final MetricRegistry registry = Mockito.mock(MetricRegistry.class);
+
+ InstrumentedAsyncClientConnectionManager.builder(registry)
+ .name("some-name")
+ .name("some-other-name")
+ .build()
+ .close();
+
+ ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(String.class);
+ Mockito.verify(registry, Mockito.atLeast(1)).register(argumentCaptor.capture(), any());
+ assertTrue(argumentCaptor.getValue().contains("some-other-name"));
+ }
+}
diff --git a/metrics-httpclient5/src/test/java/com/codahale/metrics/httpclient5/InstrumentedHttpAsyncClientsTest.java b/metrics-httpclient5/src/test/java/com/codahale/metrics/httpclient5/InstrumentedHttpAsyncClientsTest.java
new file mode 100644
index 0000000000..1a161e8d30
--- /dev/null
+++ b/metrics-httpclient5/src/test/java/com/codahale/metrics/httpclient5/InstrumentedHttpAsyncClientsTest.java
@@ -0,0 +1,150 @@
+package com.codahale.metrics.httpclient5;
+
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.MetricRegistryListener;
+import com.codahale.metrics.Timer;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
+import org.apache.hc.client5.http.async.methods.SimpleHttpRequests;
+import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.ConnectionClosedException;
+import org.apache.hc.core5.http.HttpRequest;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class InstrumentedHttpAsyncClientsTest {
+ @Rule
+ public final MockitoRule mockitoRule = MockitoJUnit.rule();
+
+ @Mock
+ private HttpClientMetricNameStrategy metricNameStrategy;
+ @Mock
+ private MetricRegistryListener registryListener;
+ private HttpServer httpServer;
+ private MetricRegistry metricRegistry;
+ private CloseableHttpAsyncClient client;
+
+
+ @Before
+ public void setUp() throws IOException {
+ httpServer = HttpServer.create(new InetSocketAddress(0), 0);
+
+ metricRegistry = new MetricRegistry();
+ metricRegistry.addListener(registryListener);
+ client = InstrumentedHttpAsyncClients.custom(metricRegistry, metricNameStrategy).disableAutomaticRetries().build();
+ client.start();
+ }
+
+ @After
+ public void tearDown() throws IOException {
+ if (client != null) {
+ client.close();
+ }
+ if (httpServer != null) {
+ httpServer.stop(0);
+ }
+ }
+
+ @Test
+ public void registersExpectedMetricsGivenNameStrategy() throws Exception {
+ final SimpleHttpRequest request = SimpleHttpRequests.get("http://localhost:" + httpServer.getAddress().getPort() + "/");
+ final String metricName = "some.made.up.metric.name";
+
+ httpServer.createContext("/", exchange -> {
+ exchange.sendResponseHeaders(200, 0L);
+ exchange.setStreams(null, null);
+ exchange.getResponseBody().write("TEST".getBytes(StandardCharsets.US_ASCII));
+ exchange.close();
+ });
+ httpServer.start();
+
+ when(metricNameStrategy.getNameFor(any(), any(HttpRequest.class))).thenReturn(metricName);
+
+ final Future responseFuture = client.execute(request, new FutureCallback() {
+ @Override
+ public void completed(SimpleHttpResponse result) {
+ assertThat(result.getBodyText()).isEqualTo("TEST");
+ }
+
+ @Override
+ public void failed(Exception ex) {
+ fail();
+ }
+
+ @Override
+ public void cancelled() {
+ fail();
+ }
+ });
+ responseFuture.get(1L, TimeUnit.SECONDS);
+
+ verify(registryListener).onTimerAdded(eq(metricName), any(Timer.class));
+ }
+
+ @Test
+ public void registersExpectedExceptionMetrics() throws Exception {
+ final CountDownLatch countDownLatch = new CountDownLatch(1);
+ final SimpleHttpRequest request = SimpleHttpRequests.get("http://localhost:" + httpServer.getAddress().getPort() + "/");
+ final String requestMetricName = "request";
+ final String exceptionMetricName = "exception";
+
+ httpServer.createContext("/", HttpExchange::close);
+ httpServer.start();
+
+ when(metricNameStrategy.getNameFor(any(), any(HttpRequest.class)))
+ .thenReturn(requestMetricName);
+ when(metricNameStrategy.getNameFor(any(), any(Exception.class)))
+ .thenReturn(exceptionMetricName);
+
+ try {
+ final Future responseFuture = client.execute(request, new FutureCallback() {
+ @Override
+ public void completed(SimpleHttpResponse result) {
+ fail();
+ }
+
+ @Override
+ public void failed(Exception ex) {
+ countDownLatch.countDown();
+ }
+
+ @Override
+ public void cancelled() {
+ fail();
+ }
+ });
+ countDownLatch.await(5, TimeUnit.SECONDS);
+ responseFuture.get(5, TimeUnit.SECONDS);
+
+ fail();
+ } catch (ExecutionException e) {
+ assertThat(e).hasCauseInstanceOf(ConnectionClosedException.class);
+ await().atMost(5, TimeUnit.SECONDS)
+ .untilAsserted(() -> assertThat(metricRegistry.getMeters()).containsKey("exception"));
+ }
+ }
+}
diff --git a/metrics-httpclient5/src/test/java/com/codahale/metrics/httpclient5/InstrumentedHttpClientConnectionManagerTest.java b/metrics-httpclient5/src/test/java/com/codahale/metrics/httpclient5/InstrumentedHttpClientConnectionManagerTest.java
new file mode 100644
index 0000000000..91fdce31da
--- /dev/null
+++ b/metrics-httpclient5/src/test/java/com/codahale/metrics/httpclient5/InstrumentedHttpClientConnectionManagerTest.java
@@ -0,0 +1,41 @@
+package com.codahale.metrics.httpclient5;
+
+import com.codahale.metrics.MetricRegistry;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import static junit.framework.TestCase.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+
+public class InstrumentedHttpClientConnectionManagerTest {
+ private final MetricRegistry metricRegistry = new MetricRegistry();
+
+ @Test
+ public void shouldRemoveGauges() {
+ final InstrumentedHttpClientConnectionManager instrumentedHttpClientConnectionManager = InstrumentedHttpClientConnectionManager.builder(metricRegistry).build();
+ Assert.assertEquals(4, metricRegistry.getGauges().size());
+
+ instrumentedHttpClientConnectionManager.close();
+ Assert.assertEquals(0, metricRegistry.getGauges().size());
+
+ // should be able to create another one with the same name ("")
+ InstrumentedHttpClientConnectionManager.builder(metricRegistry).build().close();
+ }
+
+ @Test
+ public void configurableViaBuilder() {
+ final MetricRegistry registry = Mockito.mock(MetricRegistry.class);
+
+ InstrumentedHttpClientConnectionManager.builder(registry)
+ .name("some-name")
+ .name("some-other-name")
+ .build()
+ .close();
+
+ ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(String.class);
+ Mockito.verify(registry, Mockito.atLeast(1)).register(argumentCaptor.capture(), any());
+ assertTrue(argumentCaptor.getValue().contains("some-other-name"));
+ }
+}
diff --git a/metrics-httpclient5/src/test/java/com/codahale/metrics/httpclient5/InstrumentedHttpClientsTest.java b/metrics-httpclient5/src/test/java/com/codahale/metrics/httpclient5/InstrumentedHttpClientsTest.java
new file mode 100644
index 0000000000..8d11929bfd
--- /dev/null
+++ b/metrics-httpclient5/src/test/java/com/codahale/metrics/httpclient5/InstrumentedHttpClientsTest.java
@@ -0,0 +1,77 @@
+package com.codahale.metrics.httpclient5;
+
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.MetricRegistryListener;
+import com.codahale.metrics.Timer;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.NoHttpResponseException;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.net.InetSocketAddress;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class InstrumentedHttpClientsTest {
+ private final HttpClientMetricNameStrategy metricNameStrategy =
+ mock(HttpClientMetricNameStrategy.class);
+ private final MetricRegistryListener registryListener =
+ mock(MetricRegistryListener.class);
+ private final MetricRegistry metricRegistry = new MetricRegistry();
+ private final HttpClient client =
+ InstrumentedHttpClients.custom(metricRegistry, metricNameStrategy).disableAutomaticRetries().build();
+
+ @Before
+ public void setUp() {
+ metricRegistry.addListener(registryListener);
+ }
+
+ @Test
+ public void registersExpectedMetricsGivenNameStrategy() throws Exception {
+ final HttpGet get = new HttpGet("http://example.com?q=anything");
+ final String metricName = "some.made.up.metric.name";
+
+ when(metricNameStrategy.getNameFor(any(), any(HttpRequest.class)))
+ .thenReturn(metricName);
+
+ client.execute(get);
+
+ verify(registryListener).onTimerAdded(eq(metricName), any(Timer.class));
+ }
+
+ @Test
+ public void registersExpectedExceptionMetrics() throws Exception {
+ HttpServer httpServer = HttpServer.create(new InetSocketAddress(0), 0);
+
+ final HttpGet get = new HttpGet("http://localhost:" + httpServer.getAddress().getPort() + "/");
+ final String requestMetricName = "request";
+ final String exceptionMetricName = "exception";
+
+ httpServer.createContext("/", HttpExchange::close);
+ httpServer.start();
+
+ when(metricNameStrategy.getNameFor(any(), any(HttpRequest.class)))
+ .thenReturn(requestMetricName);
+ when(metricNameStrategy.getNameFor(any(), any(Exception.class)))
+ .thenReturn(exceptionMetricName);
+
+ try {
+ client.execute(get);
+ fail();
+ } catch (NoHttpResponseException expected) {
+ assertThat(metricRegistry.getMeters()).containsKey("exception");
+ } finally {
+ httpServer.stop(0);
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index 7f1443e191..9446500ee3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -23,6 +23,7 @@
metrics-graphite
metrics-healthchecks
metrics-httpclient
+ metrics-httpclient5
metrics-httpasyncclient
metrics-jcache
metrics-jcstress