Skip to content

Commit b878526

Browse files
committed
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
1 parent 7adc2f0 commit b878526

File tree

3 files changed

+26
-17
lines changed

3 files changed

+26
-17
lines changed

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientRequestObservationContext.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,25 @@ public class ClientRequestObservationContext extends RequestReplySenderContext<C
5050
private ClientRequest request;
5151

5252

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

62+
/**
63+
* Create a new Observation context for HTTP client observations.
64+
*/
65+
public ClientRequestObservationContext(ClientRequest.Builder request) {
66+
super(ClientRequestObservationContext::setRequestHeader);
67+
setCarrier(request);
68+
setRequest(request.build());
69+
}
70+
71+
5772
private static void setRequestHeader(@Nullable ClientRequest.Builder request, String name, String value) {
5873
if (request != null) {
5974
request.headers(headers -> headers.set(name, value));

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -438,12 +438,11 @@ public <V> Flux<V> exchangeToFlux(Function<ClientResponse, ? extends Flux<V>> re
438438
@SuppressWarnings("deprecation")
439439
@Override
440440
public Mono<ClientResponse> exchange() {
441-
ClientRequestObservationContext observationContext = new ClientRequestObservationContext();
442441
ClientRequest.Builder requestBuilder = initRequestBuilder();
442+
ClientRequestObservationContext observationContext = new ClientRequestObservationContext(requestBuilder);
443443
return Mono.deferContextual(contextView -> {
444444
Observation observation = ClientHttpObservationDocumentation.HTTP_REACTIVE_CLIENT_EXCHANGES.observation(observationConvention,
445445
DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, observationRegistry);
446-
observationContext.setCarrier(requestBuilder);
447446
observation
448447
.parentObservation(contextView.getOrDefault(ObservationThreadLocalAccessor.KEY, null))
449448
.start();
@@ -452,7 +451,7 @@ public Mono<ClientResponse> exchange() {
452451
filterFunction = filterFunctions.andThen(filterFunction);
453452
}
454453
ClientRequest request = requestBuilder
455-
.attribute(ClientRequestObservationContext.CURRENT_OBSERVATION_CONTEXT_ATTRIBUTE, observation.getContext())
454+
.attribute(ClientRequestObservationContext.CURRENT_OBSERVATION_CONTEXT_ATTRIBUTE, observationContext)
456455
.build();
457456
observationContext.setUriTemplate((String) request.attribute(URI_TEMPLATE_ATTRIBUTE).orElse(null));
458457
observationContext.setRequest(request);

spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientRequestObservationConventionTests.java

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,19 @@ void shouldHaveName() {
4343

4444
@Test
4545
void shouldHaveContextualName() {
46-
ClientRequestObservationContext context = new ClientRequestObservationContext();
47-
context.setCarrier(ClientRequest.create(HttpMethod.GET, URI.create("/test")));
48-
context.setRequest(context.getCarrier().build());
46+
ClientRequestObservationContext context = new ClientRequestObservationContext(ClientRequest.create(HttpMethod.GET, URI.create("/test")));
4947
assertThat(this.observationConvention.getContextualName(context)).isEqualTo("http get");
5048
}
5149

5250
@Test
5351
void shouldOnlySupportWebClientObservationContext() {
54-
assertThat(this.observationConvention.supportsContext(new ClientRequestObservationContext())).isTrue();
52+
ClientRequest.Builder request = ClientRequest.create(HttpMethod.GET, URI.create("/test"));
53+
assertThat(this.observationConvention.supportsContext(new ClientRequestObservationContext(request))).isTrue();
5554
assertThat(this.observationConvention.supportsContext(new Observation.Context())).isFalse();
5655
}
5756

5857
@Test
58+
@SuppressWarnings("removal")
5959
void shouldAddKeyValuesForNullExchange() {
6060
ClientRequestObservationContext context = new ClientRequestObservationContext();
6161
assertThat(this.observationConvention.getLowCardinalityKeyValues(context)).hasSize(6)
@@ -68,13 +68,14 @@ void shouldAddKeyValuesForNullExchange() {
6868

6969
@Test
7070
void shouldAddKeyValuesForExchangeWithException() {
71-
ClientRequestObservationContext context = new ClientRequestObservationContext();
71+
ClientRequest.Builder request = ClientRequest.create(HttpMethod.GET, URI.create("/test"));
72+
ClientRequestObservationContext context = new ClientRequestObservationContext(request);
7273
context.setError(new IllegalStateException("Could not create client request"));
7374
assertThat(this.observationConvention.getLowCardinalityKeyValues(context)).hasSize(6)
74-
.contains(KeyValue.of("method", "none"), KeyValue.of("uri", "none"), KeyValue.of("status", "CLIENT_ERROR"),
75+
.contains(KeyValue.of("method", "GET"), KeyValue.of("uri", "none"), KeyValue.of("status", "CLIENT_ERROR"),
7576
KeyValue.of("client.name", "none"), KeyValue.of("exception", "IllegalStateException"), KeyValue.of("outcome", "UNKNOWN"));
7677
assertThat(this.observationConvention.getHighCardinalityKeyValues(context)).hasSize(1)
77-
.contains(KeyValue.of("http.url", "none"));
78+
.contains(KeyValue.of("http.url", "/test"));
7879
}
7980

8081
@Test
@@ -83,7 +84,6 @@ void shouldAddKeyValuesForRequestWithUriTemplate() {
8384
.attribute(WebClient.class.getName() + ".uriTemplate", "/resource/{id}");
8485
ClientRequestObservationContext context = createContext(request);
8586
context.setUriTemplate("/resource/{id}");
86-
context.setRequest(context.getCarrier().build());
8787
assertThat(this.observationConvention.getLowCardinalityKeyValues(context))
8888
.contains(KeyValue.of("exception", "none"), KeyValue.of("method", "GET"), KeyValue.of("uri", "/resource/{id}"),
8989
KeyValue.of("status", "200"), KeyValue.of("client.name", "none"), KeyValue.of("outcome", "SUCCESS"));
@@ -94,7 +94,6 @@ void shouldAddKeyValuesForRequestWithUriTemplate() {
9494
@Test
9595
void shouldAddKeyValuesForRequestWithoutUriTemplate() {
9696
ClientRequestObservationContext context = createContext(ClientRequest.create(HttpMethod.GET, URI.create("/resource/42")));
97-
context.setRequest(context.getCarrier().build());
9897
assertThat(this.observationConvention.getLowCardinalityKeyValues(context))
9998
.contains(KeyValue.of("method", "GET"), KeyValue.of("uri", "none"));
10099
assertThat(this.observationConvention.getHighCardinalityKeyValues(context)).hasSize(1).contains(KeyValue.of("http.url", "/resource/42"));
@@ -103,28 +102,24 @@ void shouldAddKeyValuesForRequestWithoutUriTemplate() {
103102
@Test
104103
void shouldAddClientNameKeyValueForRequestWithHost() {
105104
ClientRequestObservationContext context = createContext(ClientRequest.create(HttpMethod.GET, URI.create("https://localhost:8080/resource/42")));
106-
context.setRequest(context.getCarrier().build());
107105
assertThat(this.observationConvention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("client.name", "localhost"));
108106
}
109107

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

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

125121
private ClientRequestObservationContext createContext(ClientRequest.Builder request) {
126-
ClientRequestObservationContext context = new ClientRequestObservationContext();
127-
context.setCarrier(request);
122+
ClientRequestObservationContext context = new ClientRequestObservationContext(request);
128123
context.setResponse(ClientResponse.create(HttpStatus.OK).build());
129124
return context;
130125
}

0 commit comments

Comments
 (0)