Skip to content

Commit

Permalink
Add support for Apache HttpClient 5.x (#1543)
Browse files Browse the repository at this point in the history
  • Loading branch information
joschi authored Feb 24, 2020
1 parent c48ec0f commit 292279d
Show file tree
Hide file tree
Showing 15 changed files with 1,114 additions and 0 deletions.
59 changes: 59 additions & 0 deletions metrics-httpclient5/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-parent</artifactId>
<version>4.1.4-SNAPSHOT</version>
</parent>

<artifactId>metrics-httpclient5</artifactId>
<name>Metrics Integration for Apache HttpClient 5.x</name>
<packaging>bundle</packaging>
<description>
An Apache HttpClient 5.x wrapper providing Metrics instrumentation of connection pools, request
durations and rates, and other useful information.
</description>

<properties>
<javaModuleName>com.codahale.metrics.httpclient</javaModuleName>
<http-client.version>5.0</http-client.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-bom</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>${http-client.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.0.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -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";
}

}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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<TlsStrategy> getDefaultTlsStrategy() {
return RegistryBuilder.<TlsStrategy>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<TlsStrategy> 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<Integer>) () -> {
// this acquires a lock on the connection pool; remove if contention sucks
return getTotalStats().getAvailable();
});
metricRegistry.register(name(METRICS_PREFIX, name, "leased-connections"),
(Gauge<Integer>) () -> {
// this acquires a lock on the connection pool; remove if contention sucks
return getTotalStats().getLeased();
});
metricRegistry.register(name(METRICS_PREFIX, name, "max-connections"),
(Gauge<Integer>) () -> {
// this acquires a lock on the connection pool; remove if contention sucks
return getTotalStats().getMax();
});
metricRegistry.register(name(METRICS_PREFIX, name, "pending-connections"),
(Gauge<Integer>) () -> {
// 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<TlsStrategy> 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<TlsStrategy> 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);
}
}

}
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
Loading

0 comments on commit 292279d

Please sign in to comment.