com.turbospaces.http.CustomMicrometerHttpRequestExecutor Maven / Gradle / Ivy
/*
* Copyright 2019 VMware, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.turbospaces.http;
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.httpcomponents.ApacheHttpClientContext;
import io.micrometer.core.instrument.binder.httpcomponents.ApacheHttpClientObservationConvention;
import io.micrometer.core.instrument.binder.httpcomponents.DefaultApacheHttpClientObservationConvention;
import io.micrometer.core.instrument.binder.httpcomponents.DefaultUriMapper;
import io.micrometer.core.instrument.observation.ObservationOrTimerCompatibleInstrumentation;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import org.apache.http.HttpClientConnection;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestExecutor;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
/**
* Copy of MicrometerHttpRequestExecutor(1.2.0) with additional tag mapping functionality.
* exportTagsForRoute is disabled by default.
*/
public class CustomMicrometerHttpRequestExecutor extends HttpRequestExecutor {
static final String METER_NAME = "httpcomponents.httpclient.request";
private final MeterRegistry registry;
private final ObservationRegistry observationRegistry;
@Nullable
private final ApacheHttpClientObservationConvention convention;
private final Function uriMapper;
private final Function> additionalTagsMapper = new AdditionalTagsMapper();
private final Iterable extraTags;
private final boolean exportTagsForRoute;
/**
* Use {@link #builder(MeterRegistry)} to create an instance of this class.
*/
private CustomMicrometerHttpRequestExecutor(int waitForContinue, MeterRegistry registry,
Function uriMapper, Iterable extraTags, boolean exportTagsForRoute,
ObservationRegistry observationRegistry, @Nullable ApacheHttpClientObservationConvention convention) {
super(waitForContinue);
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());
this.exportTagsForRoute = exportTagsForRoute;
this.observationRegistry = observationRegistry;
this.convention = convention;
}
/**
* Use this method to create an instance of {@link CustomMicrometerHttpRequestExecutor}.
*
* @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 Builder builder(MeterRegistry registry) {
return new Builder(registry);
}
@Override
public HttpResponse execute(HttpRequest request, HttpClientConnection conn, HttpContext context)
throws IOException, HttpException {
ObservationOrTimerCompatibleInstrumentation sample = ObservationOrTimerCompatibleInstrumentation
.start(registry, observationRegistry,
() -> new ApacheHttpClientContext(request, context, uriMapper, exportTagsForRoute), convention,
DefaultApacheHttpClientObservationConvention.INSTANCE);
String statusCodeOrError = "UNKNOWN";
try {
HttpResponse response = super.execute(request, conn, context);
sample.setResponse(response);
statusCodeOrError = getStatusValue(response, null);
return response;
} catch (IOException | HttpException | RuntimeException e) {
statusCodeOrError = "IO_ERROR";
sample.setThrowable(e);
throw e;
} finally {
String status = statusCodeOrError;
sample.stop(METER_NAME, "Duration of Apache HttpClient request execution", () -> Tags
.of("method", getMethodString(request), "uri",
uriMapper.apply(request), "status", status)
.and(extraTags)
.and(additionalTagsMapper.apply(request)));
}
}
String getStatusValue(@Nullable HttpResponse response, Throwable error) {
if (error instanceof IOException || error instanceof HttpException || error instanceof RuntimeException) {
return "IO_ERROR";
}
return response != null ? Integer.toString(response.getStatusLine().getStatusCode()) : "CLIENT_ERROR";
}
String getMethodString(@Nullable HttpRequest request) {
return request != null && request.getRequestLine().getMethod() != null ? request.getRequestLine().getMethod()
: "UNKNOWN";
}
public static class Builder {
private final MeterRegistry registry;
private ObservationRegistry observationRegistry = ObservationRegistry.NOOP;
private int waitForContinue = HttpRequestExecutor.DEFAULT_WAIT_FOR_CONTINUE;
private Iterable extraTags = Collections.emptyList();
private Function uriMapper = new DefaultUriMapper();
private boolean exportTagsForRoute = false;
@Nullable
private ApacheHttpClientObservationConvention observationConvention;
Builder(MeterRegistry registry) {
this.registry = registry;
}
/**
* @param waitForContinue Overrides the wait for continue time for this request
* executor. See {@link HttpRequestExecutor} for details.
* @return This builder instance.
*/
public Builder waitForContinue(int waitForContinue) {
this.waitForContinue = waitForContinue;
return this;
}
/**
* These tags will not be applied when instrumentation is performed with the
* {@link Observation} API. Configure an
* {@link ApacheHttpClientObservationConvention} instead with the extra key
* values.
*
* @param tags Additional tags which should be exposed with every value.
* @return This builder instance.
* @see #observationConvention(ApacheHttpClientObservationConvention)
* @see #observationRegistry(ObservationRegistry)
* @see DefaultApacheHttpClientObservationConvention
*/
public Builder tags(Iterable tags) {
this.extraTags = tags;
return this;
}
/**
* Allows to register a mapping function for exposing request URIs. Be careful,
* exposing request URIs could result in a huge number of tag values, which could
* cause problems in your meter registry.
*
* By default, this feature is almost disabled. It only exposes values of the
* {@value DefaultUriMapper#URI_PATTERN_HEADER} HTTP header.
*
* @param uriMapper A mapper that allows mapping and exposing request paths.
* @return This builder instance.
* @see DefaultUriMapper
*/
public Builder uriMapper(Function uriMapper) {
this.uriMapper = uriMapper;
return this;
}
/**
* Allows to expose the target scheme, host and port with every metric. Be careful
* with enabling this feature: If your client accesses a huge number of remote
* servers, this would result in a huge number of tag values, which could cause
* cardinality problems.
*
* By default, this feature is disabled.
*
* @param exportTagsForRoute Set this to true, if the metrics should be tagged
* with the target route.
* @return This builder instance.
*/
public Builder exportTagsForRoute(boolean exportTagsForRoute) {
this.exportTagsForRoute = exportTagsForRoute;
return this;
}
/**
* Configure an observation registry to instrument using the {@link Observation}
* API instead of directly with a {@link Timer}.
*
* @param observationRegistry registry with which to instrument
* @return This builder instance.
* @since 1.10.0
*/
public Builder observationRegistry(ObservationRegistry observationRegistry) {
this.observationRegistry = observationRegistry;
return this;
}
/**
* Provide a custom convention to override the default convention used when
* instrumenting with the {@link Observation} API. This only takes effect when an
* {@link #observationRegistry(ObservationRegistry)} is configured.
*
* @param convention semantic convention to use
* @return This builder instance.
* @see #observationRegistry(ObservationRegistry)
* @since 1.10.0
*/
public Builder observationConvention(ApacheHttpClientObservationConvention convention) {
this.observationConvention = convention;
return this;
}
/**
* @return Creates an instance of {@link CustomMicrometerHttpRequestExecutor} with all
* the configured properties.
*/
public CustomMicrometerHttpRequestExecutor build() {
return new CustomMicrometerHttpRequestExecutor(waitForContinue, registry, uriMapper, extraTags,
exportTagsForRoute, observationRegistry, observationConvention);
}
}
}