Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use (Async) ExecChainHandler to measure IOExceptions (#3800) #3801

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package io.micrometer.core.instrument.binder.http;

import io.micrometer.common.lang.Nullable;
import io.micrometer.common.util.StringUtils;
import io.micrometer.core.annotation.Incubating;
import io.micrometer.core.instrument.Tag;
Expand Down Expand Up @@ -87,7 +88,7 @@ public static Tag status(jakarta.servlet.http.HttpServletResponse response) {
* @param exception the exception, may be {@code null}
* @return the exception tag derived from the exception
*/
public static Tag exception(Throwable exception) {
public static Tag exception(@Nullable Throwable exception) {
if (exception != null) {
String simpleName = exception.getClass().getSimpleName();
return Tag.of("exception",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.binder.http.Outcome;
import io.micrometer.core.instrument.binder.httpcomponents.hc5.ApacheHttpClientMetricsBinder;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponseInterceptor;
Expand All @@ -45,9 +46,7 @@
* .build();
* }</pre>
* <p>
* See
* {@link io.micrometer.core.instrument.binder.httpcomponents.hc5.MicrometerHttpClientInterceptor}
* for Apache HTTP client 5 support.
* See {@link ApacheHttpClientMetricsBinder} for Apache HTTP client 5 support.
*
* @author Jon Schneider
* @since 1.4.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.binder.http.Outcome;
import io.micrometer.core.instrument.binder.httpcomponents.hc5.ApacheHttpClientMetricsBinder;
import io.micrometer.core.instrument.observation.ObservationOrTimerCompatibleInstrumentation;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
Expand Down Expand Up @@ -50,9 +51,7 @@
* .build();
* </pre>
* <p>
* See
* {@link io.micrometer.core.instrument.binder.httpcomponents.hc5.MicrometerHttpRequestExecutor}
* for Apache HTTP client 5 support.
* See {@link ApacheHttpClientMetricsBinder} for Apache HTTP client 5 support.
*
* @author Benjamin Hubert (benjamin.hubert@willhaben.at)
* @author Tommy Ludwig
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,134 +18,150 @@
import io.micrometer.common.lang.Nullable;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.binder.http.Outcome;
import io.micrometer.core.instrument.observation.ObservationOrTimerCompatibleInstrumentation;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import org.apache.hc.core5.http.*;
import org.apache.hc.core5.http.impl.io.HttpRequestExecutor;
import org.apache.hc.core5.http.io.HttpClientConnection;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.util.Timeout;
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 org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.core5.http.HttpRequest;

import java.io.IOException;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;

/**
* This instruments the execution of every request that goes through an
* {@link org.apache.hc.client5.http.classic.HttpClient} on which it is configured. It
* must be registered as request executor when creating the HttpClient instance. For
* example:
* classic <pre>{@code
* HttpClientBuilder clientBuilder = HttpClientBuilder.create();
* ApacheHttpClientMetricsBinder.builder(registry)
* .build()
* .instrument(clientBuilder);
* CloseableHttpClient httpClient = clientBuilder.build();
*}</pre>
*
* <pre>
* HttpClientBuilder.create()
* .setRequestExecutor(MicrometerHttpRequestExecutor
* .builder(meterRegistry)
* .build())
* .build();
* </pre>
* async: <pre>{@code
* HttpAsyncClientBuilder asyncClientBuilder = HttpAsyncClientBuilder.create();
* ApacheHttpClientMetricsBinder.builder(registry)
* .build()
* .instrument(asyncClientBuilder);
*
* CloseableHttpAsyncClient httpAsyncClient = asyncClientBuilder.build();
* }</pre>
*
* @author Benjamin Hubert (benjamin.hubert@willhaben.at)
* @author Tommy Ludwig
* @since 1.11.0
* @author Lars Uffmann
* @since 1.12.0
*/
public class MicrometerHttpRequestExecutor extends HttpRequestExecutor {
public class ApacheHttpClientMetricsBinder {

public static final String INTERCEPTOR_NAME = "micrometer";

static final String DEFAULT_METER_NAME = "httpcomponents.httpclient.request";

static final String METER_NAME = "httpcomponents.httpclient.request";
private final String meterName;

private final MeterRegistry registry;

private final ObservationRegistry observationRegistry;

@Nullable
private final ApacheHttpClientObservationConvention convention;
private final Iterable<Tag> extraTags;

private final Function<HttpRequest, String> uriMapper;

private final Iterable<Tag> extraTags;

private final boolean exportTagsForRoute;

/**
* Use {@link #builder(MeterRegistry)} to create an instance of this class.
*/
private MicrometerHttpRequestExecutor(Timeout waitForContinue, MeterRegistry registry,
Function<HttpRequest, String> uriMapper, Iterable<Tag> extraTags, boolean exportTagsForRoute,
ObservationRegistry observationRegistry, @Nullable ApacheHttpClientObservationConvention convention) {
super(waitForContinue, null, null);
this.registry = Optional.ofNullable(registry)
.orElseThrow(() -> new IllegalArgumentException("registry is required but has been initialized with null"));
this.uriMapper = Optional.ofNullable(uriMapper)
.orElseThrow(
() -> new IllegalArgumentException("uriMapper is required but has been initialized with null"));
this.extraTags = Optional.ofNullable(extraTags).orElse(Collections.emptyList());
private final ApacheHttpClientObservationConvention observationConvention;

private final boolean meterRetries;

ApacheHttpClientMetricsBinder(String meterName, MeterRegistry registry, Function<HttpRequest, String> uriMapper,
Iterable<Tag> extraTags, boolean exportTagsForRoute, ObservationRegistry observationRegistry,
ApacheHttpClientObservationConvention observationConvention, boolean meterRetries) {
this.meterName = meterName;
this.registry = Objects.requireNonNull(registry);
this.uriMapper = Objects.requireNonNull(uriMapper);
this.extraTags = Objects.requireNonNull(extraTags);
this.exportTagsForRoute = exportTagsForRoute;
this.observationRegistry = observationRegistry;
this.convention = convention;
this.observationConvention = observationConvention;
this.meterRetries = meterRetries;
}

public void instrument(HttpClientBuilder clientBuilder) {
Objects.requireNonNull(clientBuilder);
final MeteringExecChainHandler execChainHandler = new MeteringExecChainHandler(registry, meterName, uriMapper,
extraTags, exportTagsForRoute, observationRegistry, observationConvention);
if (meterRetries) {
clientBuilder.addExecInterceptorAfter(ChainElement.RETRY.name(), INTERCEPTOR_NAME, execChainHandler);
}
else {
clientBuilder.addExecInterceptorFirst(INTERCEPTOR_NAME, execChainHandler);
}
}

/**
* Use this method to create an instance of {@link MicrometerHttpRequestExecutor}.
* @param registry The registry to register the metrics to.
* @return An instance of the builder, which allows further configuration of the
* request executor.
* Instrument the clientBuilder and immediately build the client.
* @param clientBuilder - the clientBuilder to instrument
* @return the fully configured CloseableHttpClient
*/
public static Builder builder(MeterRegistry registry) {
return new Builder(registry);
public CloseableHttpClient instrumentAndGet(HttpClientBuilder clientBuilder) {
instrument(clientBuilder);
return clientBuilder.build();
}

@Override
public ClassicHttpResponse execute(ClassicHttpRequest request, HttpClientConnection conn, HttpContext context)
throws IOException, HttpException {
ObservationOrTimerCompatibleInstrumentation<ApacheHttpClientContext> sample = ObservationOrTimerCompatibleInstrumentation
.start(registry, observationRegistry,
() -> new ApacheHttpClientContext(request, context, uriMapper, exportTagsForRoute), convention,
DefaultApacheHttpClientObservationConvention.INSTANCE);
String statusCodeOrError = "UNKNOWN";
Outcome statusOutcome = Outcome.UNKNOWN;

try {
ClassicHttpResponse response = super.execute(request, conn, context);
sample.setResponse(response);
statusCodeOrError = DefaultApacheHttpClientObservationConvention.INSTANCE.getStatusValue(response, null);
statusOutcome = DefaultApacheHttpClientObservationConvention.INSTANCE.getStatusOutcome(response);
return response;
public void instrument(HttpAsyncClientBuilder asyncClientBuilder) {
Objects.requireNonNull(asyncClientBuilder);
final MeteringAsyncExecChainHandler execChainHandler = new MeteringAsyncExecChainHandler(registry,
observationRegistry, observationConvention, meterName, uriMapper, exportTagsForRoute, extraTags,
meterRetries);
if (meterRetries) {
asyncClientBuilder.addExecInterceptorAfter(ChainElement.RETRY.name(), INTERCEPTOR_NAME, execChainHandler);
}
catch (IOException | HttpException | RuntimeException e) {
statusCodeOrError = "IO_ERROR";
sample.setThrowable(e);
throw e;
}
finally {
String status = statusCodeOrError;
String outcome = statusOutcome.name();
sample.stop(METER_NAME, "Duration of Apache HttpClient request execution",
() -> Tags
.of("method", DefaultApacheHttpClientObservationConvention.INSTANCE.getMethodString(request),
"uri", uriMapper.apply(request), "status", status, "outcome", outcome)
.and(exportTagsForRoute ? HttpContextUtils.generateTagsForRoute(context) : Tags.empty())
.and(extraTags));
else {
asyncClientBuilder.addExecInterceptorFirst(INTERCEPTOR_NAME, execChainHandler);
}
}

/**
* Instrument the asyncClientBuilder and immediately build the client.
* @param asyncClientBuilder - the asyncClientBuilder to instrument
* @return the fully configured CloseableHttpAsyncClient
*/
public CloseableHttpAsyncClient instrumentAndGet(HttpAsyncClientBuilder asyncClientBuilder) {
instrument(asyncClientBuilder);
return asyncClientBuilder.build();
}

/**
* Use this method to create an instance of {@link ApacheHttpClientMetricsBinder}.
* @param registry The registry to register the metrics to.
* @return An instance of the builder, which allows further configuration of the
* request executor.
*/
public static ApacheHttpClientMetricsBinder.Builder builder(MeterRegistry registry) {
return new ApacheHttpClientMetricsBinder.Builder(registry);
}

public static class Builder {

private final MeterRegistry registry;

private ObservationRegistry observationRegistry = ObservationRegistry.NOOP;
private String meterName = DEFAULT_METER_NAME;

private Timeout waitForContinue = HttpRequestExecutor.DEFAULT_WAIT_FOR_CONTINUE;
private ObservationRegistry observationRegistry = ObservationRegistry.NOOP;

private Iterable<Tag> extraTags = Collections.emptyList();

private Function<HttpRequest, String> uriMapper = new DefaultUriMapper();

private boolean exportTagsForRoute = false;

private boolean meterRetries = false;

@Nullable
private ApacheHttpClientObservationConvention observationConvention;

Expand All @@ -154,12 +170,13 @@ public static class Builder {
}

/**
* @param waitForContinue Overrides the wait for continue time for this request
* executor. See {@link HttpRequestExecutor} for details.
* Measurements will be exported using the supplier meterName.
* @param meterName - the meterName
* @return This builder instance.
*/
public Builder waitForContinue(Timeout waitForContinue) {
this.waitForContinue = waitForContinue;
public Builder meterName(String meterName) {
// Assert.hasText() ...
this.meterName = Objects.requireNonNull(meterName);
return this;
}

Expand All @@ -175,7 +192,7 @@ public Builder waitForContinue(Timeout waitForContinue) {
* @see DefaultApacheHttpClientObservationConvention
*/
public Builder tags(Iterable<Tag> tags) {
this.extraTags = tags;
this.extraTags = Optional.ofNullable(tags).orElse(Collections.emptyList());
return this;
}

Expand Down Expand Up @@ -236,12 +253,24 @@ public Builder observationConvention(ApacheHttpClientObservationConvention conve
}

/**
* @return Creates an instance of {@link MicrometerHttpRequestExecutor} with all
* Apache HttpClient has a build in retry facility wqhich is active by default.
* This parameter decides whether to monitor retries individually or as a single
* observation.
* @param meterRetries whether to meter retries
* @return This builder instance.
*/
public Builder meterRetries(boolean meterRetries) {
this.meterRetries = meterRetries;
return this;
}

/**
* @return Creates an instance of {@link ApacheHttpClientMetricsBinder} with all
* the configured properties.
*/
public MicrometerHttpRequestExecutor build() {
return new MicrometerHttpRequestExecutor(waitForContinue, registry, uriMapper, extraTags,
exportTagsForRoute, observationRegistry, observationConvention);
public ApacheHttpClientMetricsBinder build() {
return new ApacheHttpClientMetricsBinder(meterName, registry, uriMapper, extraTags, exportTagsForRoute,
observationRegistry, observationConvention, meterRetries);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
* {@link ObservationDocumentation} for Apache HTTP Client 5 instrumentation.
*
* @since 1.11.0
* @see MicrometerHttpRequestExecutor
* @see ApacheHttpClientMetricsBinder
*/
public enum ApacheHttpClientObservationDocumentation implements ObservationDocumentation {

Expand Down Expand Up @@ -71,6 +71,16 @@ public String asString() {
return "uri";
}
},
/**
* Key name for exception.
* @since 1.11.0
*/
EXCEPTION {
@Override
public String asString() {
return "exception";
}
},
TARGET_SCHEME {
@Override
public String asString() {
Expand Down
Loading