Improve "active" metrics handling in WebClient observations
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
This commit is contained in:
parent
7adc2f0779
commit
b87852612b
|
|
@ -50,10 +50,25 @@ public class ClientRequestObservationContext extends RequestReplySenderContext<C
|
||||||
private ClientRequest request;
|
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() {
|
public ClientRequestObservationContext() {
|
||||||
super(ClientRequestObservationContext::setRequestHeader);
|
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) {
|
private static void setRequestHeader(@Nullable ClientRequest.Builder request, String name, String value) {
|
||||||
if (request != null) {
|
if (request != null) {
|
||||||
request.headers(headers -> headers.set(name, value));
|
request.headers(headers -> headers.set(name, value));
|
||||||
|
|
|
||||||
|
|
@ -438,12 +438,11 @@ final class DefaultWebClient implements WebClient {
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
@Override
|
@Override
|
||||||
public Mono<ClientResponse> exchange() {
|
public Mono<ClientResponse> exchange() {
|
||||||
ClientRequestObservationContext observationContext = new ClientRequestObservationContext();
|
|
||||||
ClientRequest.Builder requestBuilder = initRequestBuilder();
|
ClientRequest.Builder requestBuilder = initRequestBuilder();
|
||||||
|
ClientRequestObservationContext observationContext = new ClientRequestObservationContext(requestBuilder);
|
||||||
return Mono.deferContextual(contextView -> {
|
return Mono.deferContextual(contextView -> {
|
||||||
Observation observation = ClientHttpObservationDocumentation.HTTP_REACTIVE_CLIENT_EXCHANGES.observation(observationConvention,
|
Observation observation = ClientHttpObservationDocumentation.HTTP_REACTIVE_CLIENT_EXCHANGES.observation(observationConvention,
|
||||||
DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, observationRegistry);
|
DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, observationRegistry);
|
||||||
observationContext.setCarrier(requestBuilder);
|
|
||||||
observation
|
observation
|
||||||
.parentObservation(contextView.getOrDefault(ObservationThreadLocalAccessor.KEY, null))
|
.parentObservation(contextView.getOrDefault(ObservationThreadLocalAccessor.KEY, null))
|
||||||
.start();
|
.start();
|
||||||
|
|
@ -452,7 +451,7 @@ final class DefaultWebClient implements WebClient {
|
||||||
filterFunction = filterFunctions.andThen(filterFunction);
|
filterFunction = filterFunctions.andThen(filterFunction);
|
||||||
}
|
}
|
||||||
ClientRequest request = requestBuilder
|
ClientRequest request = requestBuilder
|
||||||
.attribute(ClientRequestObservationContext.CURRENT_OBSERVATION_CONTEXT_ATTRIBUTE, observation.getContext())
|
.attribute(ClientRequestObservationContext.CURRENT_OBSERVATION_CONTEXT_ATTRIBUTE, observationContext)
|
||||||
.build();
|
.build();
|
||||||
observationContext.setUriTemplate((String) request.attribute(URI_TEMPLATE_ATTRIBUTE).orElse(null));
|
observationContext.setUriTemplate((String) request.attribute(URI_TEMPLATE_ATTRIBUTE).orElse(null));
|
||||||
observationContext.setRequest(request);
|
observationContext.setRequest(request);
|
||||||
|
|
|
||||||
|
|
@ -43,19 +43,19 @@ class DefaultClientRequestObservationConventionTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldHaveContextualName() {
|
void shouldHaveContextualName() {
|
||||||
ClientRequestObservationContext context = new ClientRequestObservationContext();
|
ClientRequestObservationContext context = new ClientRequestObservationContext(ClientRequest.create(HttpMethod.GET, URI.create("/test")));
|
||||||
context.setCarrier(ClientRequest.create(HttpMethod.GET, URI.create("/test")));
|
|
||||||
context.setRequest(context.getCarrier().build());
|
|
||||||
assertThat(this.observationConvention.getContextualName(context)).isEqualTo("http get");
|
assertThat(this.observationConvention.getContextualName(context)).isEqualTo("http get");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldOnlySupportWebClientObservationContext() {
|
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();
|
assertThat(this.observationConvention.supportsContext(new Observation.Context())).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@SuppressWarnings("removal")
|
||||||
void shouldAddKeyValuesForNullExchange() {
|
void shouldAddKeyValuesForNullExchange() {
|
||||||
ClientRequestObservationContext context = new ClientRequestObservationContext();
|
ClientRequestObservationContext context = new ClientRequestObservationContext();
|
||||||
assertThat(this.observationConvention.getLowCardinalityKeyValues(context)).hasSize(6)
|
assertThat(this.observationConvention.getLowCardinalityKeyValues(context)).hasSize(6)
|
||||||
|
|
@ -68,13 +68,14 @@ class DefaultClientRequestObservationConventionTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldAddKeyValuesForExchangeWithException() {
|
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"));
|
context.setError(new IllegalStateException("Could not create client request"));
|
||||||
assertThat(this.observationConvention.getLowCardinalityKeyValues(context)).hasSize(6)
|
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"));
|
KeyValue.of("client.name", "none"), KeyValue.of("exception", "IllegalStateException"), KeyValue.of("outcome", "UNKNOWN"));
|
||||||
assertThat(this.observationConvention.getHighCardinalityKeyValues(context)).hasSize(1)
|
assertThat(this.observationConvention.getHighCardinalityKeyValues(context)).hasSize(1)
|
||||||
.contains(KeyValue.of("http.url", "none"));
|
.contains(KeyValue.of("http.url", "/test"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -83,7 +84,6 @@ class DefaultClientRequestObservationConventionTests {
|
||||||
.attribute(WebClient.class.getName() + ".uriTemplate", "/resource/{id}");
|
.attribute(WebClient.class.getName() + ".uriTemplate", "/resource/{id}");
|
||||||
ClientRequestObservationContext context = createContext(request);
|
ClientRequestObservationContext context = createContext(request);
|
||||||
context.setUriTemplate("/resource/{id}");
|
context.setUriTemplate("/resource/{id}");
|
||||||
context.setRequest(context.getCarrier().build());
|
|
||||||
assertThat(this.observationConvention.getLowCardinalityKeyValues(context))
|
assertThat(this.observationConvention.getLowCardinalityKeyValues(context))
|
||||||
.contains(KeyValue.of("exception", "none"), KeyValue.of("method", "GET"), KeyValue.of("uri", "/resource/{id}"),
|
.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"));
|
KeyValue.of("status", "200"), KeyValue.of("client.name", "none"), KeyValue.of("outcome", "SUCCESS"));
|
||||||
|
|
@ -94,7 +94,6 @@ class DefaultClientRequestObservationConventionTests {
|
||||||
@Test
|
@Test
|
||||||
void shouldAddKeyValuesForRequestWithoutUriTemplate() {
|
void shouldAddKeyValuesForRequestWithoutUriTemplate() {
|
||||||
ClientRequestObservationContext context = createContext(ClientRequest.create(HttpMethod.GET, URI.create("/resource/42")));
|
ClientRequestObservationContext context = createContext(ClientRequest.create(HttpMethod.GET, URI.create("/resource/42")));
|
||||||
context.setRequest(context.getCarrier().build());
|
|
||||||
assertThat(this.observationConvention.getLowCardinalityKeyValues(context))
|
assertThat(this.observationConvention.getLowCardinalityKeyValues(context))
|
||||||
.contains(KeyValue.of("method", "GET"), KeyValue.of("uri", "none"));
|
.contains(KeyValue.of("method", "GET"), KeyValue.of("uri", "none"));
|
||||||
assertThat(this.observationConvention.getHighCardinalityKeyValues(context)).hasSize(1).contains(KeyValue.of("http.url", "/resource/42"));
|
assertThat(this.observationConvention.getHighCardinalityKeyValues(context)).hasSize(1).contains(KeyValue.of("http.url", "/resource/42"));
|
||||||
|
|
@ -103,14 +102,12 @@ class DefaultClientRequestObservationConventionTests {
|
||||||
@Test
|
@Test
|
||||||
void shouldAddClientNameKeyValueForRequestWithHost() {
|
void shouldAddClientNameKeyValueForRequestWithHost() {
|
||||||
ClientRequestObservationContext context = createContext(ClientRequest.create(HttpMethod.GET, URI.create("https://localhost:8080/resource/42")));
|
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"));
|
assertThat(this.observationConvention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("client.name", "localhost"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldAddRootUriEvenIfTemplateMissing() {
|
void shouldAddRootUriEvenIfTemplateMissing() {
|
||||||
ClientRequestObservationContext context = createContext(ClientRequest.create(HttpMethod.GET, URI.create("https://example.org/")));
|
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", "/"));
|
assertThat(this.observationConvention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("uri", "/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,13 +115,11 @@ class DefaultClientRequestObservationConventionTests {
|
||||||
void shouldOnlyConsiderPathForUriKeyValue() {
|
void shouldOnlyConsiderPathForUriKeyValue() {
|
||||||
ClientRequestObservationContext context = createContext(ClientRequest.create(HttpMethod.GET, URI.create("https://example.org/resource/42")));
|
ClientRequestObservationContext context = createContext(ClientRequest.create(HttpMethod.GET, URI.create("https://example.org/resource/42")));
|
||||||
context.setUriTemplate("https://example.org/resource/{id}");
|
context.setUriTemplate("https://example.org/resource/{id}");
|
||||||
context.setRequest(context.getCarrier().build());
|
|
||||||
assertThat(this.observationConvention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("uri", "/resource/{id}"));
|
assertThat(this.observationConvention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("uri", "/resource/{id}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private ClientRequestObservationContext createContext(ClientRequest.Builder request) {
|
private ClientRequestObservationContext createContext(ClientRequest.Builder request) {
|
||||||
ClientRequestObservationContext context = new ClientRequestObservationContext();
|
ClientRequestObservationContext context = new ClientRequestObservationContext(request);
|
||||||
context.setCarrier(request);
|
|
||||||
context.setResponse(ClientResponse.create(HttpStatus.OK).build());
|
context.setResponse(ClientResponse.create(HttpStatus.OK).build());
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue