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