Instrument RestClient for Observability

This commit instruments the new `RestClient` HTTP client for
observability. Since this client is sharing its HTTP infrastructure with
`RestTemplate` and operates on the same request/response types, this
instrumentation reuses the Observation convention and context.
This choice makes sense since one can build a new `RestClient` instance
using a `RestTemplate` instance, effectively reusing the underlying
configuration.

Closes gh-31114
This commit is contained in:
Brian Clozel
2023-08-25 16:23:04 +02:00
parent 22fd6df711
commit 35fc2df948
6 changed files with 298 additions and 1 deletions

View File

@@ -31,6 +31,8 @@ import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -49,6 +51,10 @@ import org.springframework.http.client.ClientHttpRequestInitializer;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.InterceptingClientHttpRequestFactory;
import org.springframework.http.client.observation.ClientHttpObservationDocumentation;
import org.springframework.http.client.observation.ClientRequestObservationContext;
import org.springframework.http.client.observation.ClientRequestObservationConvention;
import org.springframework.http.client.observation.DefaultClientRequestObservationConvention;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
@@ -72,6 +78,8 @@ final class DefaultRestClient implements RestClient {
private static final Log logger = LogFactory.getLog(DefaultRestClient.class);
private static final ClientRequestObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultClientRequestObservationConvention();
private static final String URI_TEMPLATE_ATTRIBUTE = RestClient.class.getName() + ".uriTemplate";
@@ -97,6 +105,8 @@ final class DefaultRestClient implements RestClient {
private final List<HttpMessageConverter<?>> messageConverters;
private final ObservationRegistry observationRegistry;
DefaultRestClient(ClientHttpRequestFactory clientRequestFactory,
@Nullable List<ClientHttpRequestInterceptor> interceptors,
@@ -105,6 +115,7 @@ final class DefaultRestClient implements RestClient {
@Nullable HttpHeaders defaultHeaders,
@Nullable List<StatusHandler> statusHandlers,
List<HttpMessageConverter<?>> messageConverters,
ObservationRegistry observationRegistry,
DefaultRestClientBuilder builder) {
this.clientRequestFactory = clientRequestFactory;
@@ -114,6 +125,7 @@ final class DefaultRestClient implements RestClient {
this.defaultHeaders = defaultHeaders;
this.defaultStatusHandlers = (statusHandlers != null ? new ArrayList<>(statusHandlers) : new ArrayList<>());
this.messageConverters = messageConverters;
this.observationRegistry = observationRegistry;
this.builder = builder;
}
@@ -372,12 +384,17 @@ final class DefaultRestClient implements RestClient {
Assert.notNull(exchangeFunction, "ExchangeFunction must not be null");
ClientHttpResponse clientResponse = null;
Observation observation = null;
URI uri = null;
try {
uri = initUri();
HttpHeaders headers = initHeaders();
ClientHttpRequest clientRequest = createRequest(uri);
clientRequest.getHeaders().addAll(headers);
ClientRequestObservationContext observationContext = new ClientRequestObservationContext(clientRequest);
observationContext.setUriTemplate((String) this.attributes.get(URI_TEMPLATE_ATTRIBUTE));
observation = ClientHttpObservationDocumentation.HTTP_CLIENT_EXCHANGES.observation(null,
DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, observationRegistry).start();
if (this.body != null) {
this.body.writeTo(clientRequest);
}
@@ -385,15 +402,29 @@ final class DefaultRestClient implements RestClient {
this.httpRequestConsumer.accept(clientRequest);
}
clientResponse = clientRequest.execute();
observationContext.setResponse(clientResponse);
return exchangeFunction.exchange(clientRequest, clientResponse);
}
catch (IOException ex) {
throw createResourceAccessException(uri, this.httpMethod, ex);
ResourceAccessException resourceAccessException = createResourceAccessException(uri, this.httpMethod, ex);
if (observation != null) {
observation.error(resourceAccessException);
}
throw resourceAccessException;
}
catch (Throwable error) {
if (observation != null) {
observation.error(error);
}
throw error;
}
finally {
if (close && clientResponse != null) {
clientResponse.close();
}
if (observation != null) {
observation.stop();
}
}
}

View File

@@ -24,6 +24,8 @@ import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Predicate;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.client.ClientHttpRequestFactory;
@@ -128,6 +130,8 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
@Nullable
private List<ClientHttpRequestInitializer> initializers;
private ObservationRegistry observationRegistry = ObservationRegistry.NOOP;
public DefaultRestClientBuilder() {
}
@@ -156,6 +160,7 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
this.interceptors = (other.interceptors != null) ? new ArrayList<>(other.interceptors) : null;
this.initializers = (other.initializers != null) ? new ArrayList<>(other.initializers) : null;
this.observationRegistry = other.observationRegistry;
}
public DefaultRestClientBuilder(RestTemplate restTemplate) {
@@ -176,6 +181,7 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
if (!CollectionUtils.isEmpty(restTemplate.getClientHttpRequestInitializers())) {
this.initializers = new ArrayList<>(restTemplate.getClientHttpRequestInitializers());
}
this.observationRegistry = restTemplate.getObservationRegistry();
}
@@ -294,6 +300,13 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
return this;
}
@Override
public RestClient.Builder observationRegistry(ObservationRegistry observationRegistry) {
Assert.notNull(observationRegistry, "observationRegistry must not be null");
this.observationRegistry = observationRegistry;
return this;
}
@Override
public RestClient.Builder apply(Consumer<RestClient.Builder> builderConsumer) {
builderConsumer.accept(this);
@@ -348,6 +361,7 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
defaultHeaders,
this.statusHandlers,
messageConverters,
this.observationRegistry,
new DefaultRestClientBuilder(this)
);
}

View File

@@ -27,6 +27,8 @@ import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@@ -364,6 +366,14 @@ public interface RestClient {
*/
Builder messageConverters(Consumer<List<HttpMessageConverter<?>>> configurer);
/**
* Configure the {@link io.micrometer.observation.ObservationRegistry} to use
* for recording HTTP client observations.
* @param observationRegistry the observation registry to use
* @return this builder
*/
Builder observationRegistry(ObservationRegistry observationRegistry);
/**
* Apply the given {@code Consumer} to this builder instance.
* <p>This can be useful for applying pre-packaged customizations.

View File

@@ -353,6 +353,14 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
this.observationRegistry = observationRegistry;
}
/**
* Return the configured {@link ObservationRegistry}.
* @since 6.1
*/
public ObservationRegistry getObservationRegistry() {
return this.observationRegistry;
}
/**
* Configure an {@link ObservationConvention} that sets the name of the
* {@link Observation observation} as well as its {@link io.micrometer.common.KeyValues}