Skip to content

Commit

Permalink
Improve "active" metrics handling in WebClient observations
Browse files Browse the repository at this point in the history
Prior to this commit, the WebClient observations would have a specific
lifecycle where the observation context is build with a
`ClientRequest.Builder` as tracing needs to add an outgoing request
header before the request is made immutable.

With this setup, the metrics observation handler processes the start
event by increasing the "http.client.requests.active" counter and
collecting tags at this point. Because then the immutable request is not
yet fully built or set on the context, the keyvalues collected by the
observation convention at that point can be incomplete.

This commit ensures that a request is always made available in the
context, even if it is updated right after the observation start. The
only difference between the two should be additional tracing headers and
a request attribute holding the current observation context.

Closes gh-31702
  • Loading branch information
bclozel committed Dec 12, 2023
1 parent 7adc2f0 commit b878526
Show file tree
Hide file tree
Showing 3 changed files with 26 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,25 @@ public class ClientRequestObservationContext extends RequestReplySenderContext<C
private ClientRequest request;


/**
* Create a new Observation context for HTTP client observations.
* @deprecated as of 6.1, in favor of {@link #ClientRequestObservationContext(ClientRequest.Builder)}
*/
@Deprecated(since = "6.1", forRemoval = true)
public ClientRequestObservationContext() {
super(ClientRequestObservationContext::setRequestHeader);
}

/**
* Create a new Observation context for HTTP client observations.
*/
public ClientRequestObservationContext(ClientRequest.Builder request) {
super(ClientRequestObservationContext::setRequestHeader);
setCarrier(request);
setRequest(request.build());
}


private static void setRequestHeader(@Nullable ClientRequest.Builder request, String name, String value) {
if (request != null) {
request.headers(headers -> headers.set(name, value));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -438,12 +438,11 @@ public <V> Flux<V> exchangeToFlux(Function<ClientResponse, ? extends Flux<V>> re
@SuppressWarnings("deprecation")
@Override
public Mono<ClientResponse> exchange() {
ClientRequestObservationContext observationContext = new ClientRequestObservationContext();
ClientRequest.Builder requestBuilder = initRequestBuilder();
ClientRequestObservationContext observationContext = new ClientRequestObservationContext(requestBuilder);
return Mono.deferContextual(contextView -> {
Observation observation = ClientHttpObservationDocumentation.HTTP_REACTIVE_CLIENT_EXCHANGES.observation(observationConvention,
DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, observationRegistry);
observationContext.setCarrier(requestBuilder);
observation
.parentObservation(contextView.getOrDefault(ObservationThreadLocalAccessor.KEY, null))
.start();
Expand All @@ -452,7 +451,7 @@ public Mono<ClientResponse> exchange() {
filterFunction = filterFunctions.andThen(filterFunction);
}
ClientRequest request = requestBuilder
.attribute(ClientRequestObservationContext.CURRENT_OBSERVATION_CONTEXT_ATTRIBUTE, observation.getContext())
.attribute(ClientRequestObservationContext.CURRENT_OBSERVATION_CONTEXT_ATTRIBUTE, observationContext)
.build();
observationContext.setUriTemplate((String) request.attribute(URI_TEMPLATE_ATTRIBUTE).orElse(null));
observationContext.setRequest(request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,19 @@ void shouldHaveName() {

@Test
void shouldHaveContextualName() {
ClientRequestObservationContext context = new ClientRequestObservationContext();
context.setCarrier(ClientRequest.create(HttpMethod.GET, URI.create("/test")));
context.setRequest(context.getCarrier().build());
ClientRequestObservationContext context = new ClientRequestObservationContext(ClientRequest.create(HttpMethod.GET, URI.create("/test")));
assertThat(this.observationConvention.getContextualName(context)).isEqualTo("http get");
}

@Test
void shouldOnlySupportWebClientObservationContext() {
assertThat(this.observationConvention.supportsContext(new ClientRequestObservationContext())).isTrue();
ClientRequest.Builder request = ClientRequest.create(HttpMethod.GET, URI.create("/test"));
assertThat(this.observationConvention.supportsContext(new ClientRequestObservationContext(request))).isTrue();
assertThat(this.observationConvention.supportsContext(new Observation.Context())).isFalse();
}

@Test
@SuppressWarnings("removal")
void shouldAddKeyValuesForNullExchange() {
ClientRequestObservationContext context = new ClientRequestObservationContext();
assertThat(this.observationConvention.getLowCardinalityKeyValues(context)).hasSize(6)
Expand All @@ -68,13 +68,14 @@ void shouldAddKeyValuesForNullExchange() {

@Test
void shouldAddKeyValuesForExchangeWithException() {
ClientRequestObservationContext context = new ClientRequestObservationContext();
ClientRequest.Builder request = ClientRequest.create(HttpMethod.GET, URI.create("/test"));
ClientRequestObservationContext context = new ClientRequestObservationContext(request);
context.setError(new IllegalStateException("Could not create client request"));
assertThat(this.observationConvention.getLowCardinalityKeyValues(context)).hasSize(6)
.contains(KeyValue.of("method", "none"), KeyValue.of("uri", "none"), KeyValue.of("status", "CLIENT_ERROR"),
.contains(KeyValue.of("method", "GET"), KeyValue.of("uri", "none"), KeyValue.of("status", "CLIENT_ERROR"),
KeyValue.of("client.name", "none"), KeyValue.of("exception", "IllegalStateException"), KeyValue.of("outcome", "UNKNOWN"));
assertThat(this.observationConvention.getHighCardinalityKeyValues(context)).hasSize(1)
.contains(KeyValue.of("http.url", "none"));
.contains(KeyValue.of("http.url", "/test"));
}

@Test
Expand All @@ -83,7 +84,6 @@ void shouldAddKeyValuesForRequestWithUriTemplate() {
.attribute(WebClient.class.getName() + ".uriTemplate", "/resource/{id}");
ClientRequestObservationContext context = createContext(request);
context.setUriTemplate("/resource/{id}");
context.setRequest(context.getCarrier().build());
assertThat(this.observationConvention.getLowCardinalityKeyValues(context))
.contains(KeyValue.of("exception", "none"), KeyValue.of("method", "GET"), KeyValue.of("uri", "/resource/{id}"),
KeyValue.of("status", "200"), KeyValue.of("client.name", "none"), KeyValue.of("outcome", "SUCCESS"));
Expand All @@ -94,7 +94,6 @@ void shouldAddKeyValuesForRequestWithUriTemplate() {
@Test
void shouldAddKeyValuesForRequestWithoutUriTemplate() {
ClientRequestObservationContext context = createContext(ClientRequest.create(HttpMethod.GET, URI.create("/resource/42")));
context.setRequest(context.getCarrier().build());
assertThat(this.observationConvention.getLowCardinalityKeyValues(context))
.contains(KeyValue.of("method", "GET"), KeyValue.of("uri", "none"));
assertThat(this.observationConvention.getHighCardinalityKeyValues(context)).hasSize(1).contains(KeyValue.of("http.url", "/resource/42"));
Expand All @@ -103,28 +102,24 @@ void shouldAddKeyValuesForRequestWithoutUriTemplate() {
@Test
void shouldAddClientNameKeyValueForRequestWithHost() {
ClientRequestObservationContext context = createContext(ClientRequest.create(HttpMethod.GET, URI.create("https://localhost:8080/resource/42")));
context.setRequest(context.getCarrier().build());
assertThat(this.observationConvention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("client.name", "localhost"));
}

@Test
void shouldAddRootUriEvenIfTemplateMissing() {
ClientRequestObservationContext context = createContext(ClientRequest.create(HttpMethod.GET, URI.create("https://example.org/")));
context.setRequest(context.getCarrier().build());
assertThat(this.observationConvention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("uri", "/"));
}

@Test
void shouldOnlyConsiderPathForUriKeyValue() {
ClientRequestObservationContext context = createContext(ClientRequest.create(HttpMethod.GET, URI.create("https://example.org/resource/42")));
context.setUriTemplate("https://example.org/resource/{id}");
context.setRequest(context.getCarrier().build());
assertThat(this.observationConvention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("uri", "/resource/{id}"));
}

private ClientRequestObservationContext createContext(ClientRequest.Builder request) {
ClientRequestObservationContext context = new ClientRequestObservationContext();
context.setCarrier(request);
ClientRequestObservationContext context = new ClientRequestObservationContext(request);
context.setResponse(ClientResponse.create(HttpStatus.OK).build());
return context;
}
Expand Down

0 comments on commit b878526

Please sign in to comment.