Skip to content

Commit 88685ef

Browse files
authored
Apply gh-5553 to io.micrometer.core.instrument.binder.jdk (#5704)
* Apply gh-5553 to io.micrometer.core.instrument.binder.jdk * Fix missing error on context in MicrometerHttpClient.sendAsync() This commit also applies gh-5705.
1 parent 53a7c52 commit 88685ef

File tree

3 files changed

+95
-32
lines changed

3 files changed

+95
-32
lines changed

micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/DefaultHttpClientObservationConvention.java

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,29 +43,36 @@ public KeyValues getLowCardinalityKeyValues(HttpClientContext context) {
4343
return KeyValues.empty();
4444
}
4545
HttpRequest httpRequest = context.getCarrier().build();
46-
KeyValues keyValues = KeyValues.of(
46+
return KeyValues.of(
4747
HttpClientObservationDocumentation.LowCardinalityKeys.METHOD.withValue(httpRequest.method()),
4848
HttpClientObservationDocumentation.LowCardinalityKeys.URI
49-
.withValue(getUriTag(httpRequest, context.getResponse(), context.getUriMapper())));
50-
if (context.getResponse() != null) {
51-
keyValues = keyValues
52-
.and(HttpClientObservationDocumentation.LowCardinalityKeys.STATUS
53-
.withValue(String.valueOf(context.getResponse().statusCode())))
54-
.and(HttpClientObservationDocumentation.LowCardinalityKeys.OUTCOME
55-
.withValue(Outcome.forStatus(context.getResponse().statusCode()).name()));
56-
}
57-
return keyValues;
49+
.withValue(getUriTag(httpRequest, context.getResponse(), context.getUriMapper())),
50+
HttpClientObservationDocumentation.LowCardinalityKeys.STATUS
51+
.withValue(getStatus(context.getResponse())),
52+
HttpClientObservationDocumentation.LowCardinalityKeys.OUTCOME
53+
.withValue(getOutcome(context.getResponse())));
5854
}
5955

60-
String getUriTag(@Nullable HttpRequest request, @Nullable HttpResponse<?> httpResponse,
56+
String getUriTag(HttpRequest request, @Nullable HttpResponse<?> httpResponse,
6157
Function<HttpRequest, String> uriMapper) {
62-
if (request == null) {
63-
return null;
64-
}
6558
return httpResponse != null && (httpResponse.statusCode() == 404 || httpResponse.statusCode() == 301)
6659
? "NOT_FOUND" : uriMapper.apply(request);
6760
}
6861

62+
String getStatus(@Nullable HttpResponse<?> response) {
63+
if (response == null) {
64+
return "UNKNOWN";
65+
}
66+
return String.valueOf(response.statusCode());
67+
}
68+
69+
String getOutcome(@Nullable HttpResponse<?> response) {
70+
if (response == null) {
71+
return Outcome.UNKNOWN.name();
72+
}
73+
return Outcome.forStatus(response.statusCode()).name();
74+
}
75+
6976
@Override
7077
@NonNull
7178
public String getName() {

micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/MicrometerHttpClient.java

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@
1717

1818
import io.micrometer.common.lang.Nullable;
1919
import io.micrometer.core.instrument.MeterRegistry;
20-
import io.micrometer.core.instrument.Tag;
2120
import io.micrometer.core.instrument.Tags;
22-
import io.micrometer.core.instrument.binder.http.Outcome;
2321
import io.micrometer.core.instrument.observation.ObservationOrTimerCompatibleInstrumentation;
2422
import io.micrometer.observation.Observation;
2523
import io.micrometer.observation.ObservationRegistry;
@@ -36,6 +34,7 @@
3634
import java.time.Duration;
3735
import java.util.Optional;
3836
import java.util.concurrent.CompletableFuture;
37+
import java.util.concurrent.CompletionException;
3938
import java.util.concurrent.Executor;
4039
import java.util.function.Function;
4140

@@ -234,19 +233,13 @@ private <T> void stopObservationOrTimer(
234233
ObservationOrTimerCompatibleInstrumentation<HttpClientContext> instrumentation, HttpRequest request,
235234
@Nullable HttpResponse<T> res) {
236235
instrumentation.stop(DefaultHttpClientObservationConvention.INSTANCE.getName(), "Timer for JDK's HttpClient",
237-
() -> {
238-
Tags tags = Tags.of(HttpClientObservationDocumentation.LowCardinalityKeys.METHOD.asString(),
239-
request.method(), HttpClientObservationDocumentation.LowCardinalityKeys.URI.asString(),
240-
DefaultHttpClientObservationConvention.INSTANCE.getUriTag(request, res, uriMapper));
241-
if (res != null) {
242-
tags = tags
243-
.and(Tag.of(HttpClientObservationDocumentation.LowCardinalityKeys.STATUS.asString(),
244-
String.valueOf(res.statusCode())))
245-
.and(Tag.of(HttpClientObservationDocumentation.LowCardinalityKeys.OUTCOME.asString(),
246-
Outcome.forStatus(res.statusCode()).name()));
247-
}
248-
return tags;
249-
});
236+
() -> Tags.of(HttpClientObservationDocumentation.LowCardinalityKeys.METHOD.asString(), request.method(),
237+
HttpClientObservationDocumentation.LowCardinalityKeys.URI.asString(),
238+
DefaultHttpClientObservationConvention.INSTANCE.getUriTag(request, res, uriMapper),
239+
HttpClientObservationDocumentation.LowCardinalityKeys.STATUS.asString(),
240+
DefaultHttpClientObservationConvention.INSTANCE.getStatus(res),
241+
HttpClientObservationDocumentation.LowCardinalityKeys.OUTCOME.asString(),
242+
DefaultHttpClientObservationConvention.INSTANCE.getOutcome(res)));
250243
}
251244

252245
private ObservationOrTimerCompatibleInstrumentation<HttpClientContext> observationOrTimer(
@@ -272,12 +265,16 @@ public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest httpRequest,
272265
httpRequestBuilder);
273266
HttpRequest request = httpRequestBuilder.build();
274267
return client.sendAsync(request, bodyHandler, pushPromiseHandler).handle((response, throwable) -> {
268+
instrumentation.setResponse(response);
275269
if (throwable != null) {
276270
instrumentation.setThrowable(throwable);
271+
stopObservationOrTimer(instrumentation, request, response);
272+
throw new CompletionException(throwable);
273+
}
274+
else {
275+
stopObservationOrTimer(instrumentation, request, response);
276+
return response;
277277
}
278-
instrumentation.setResponse(response);
279-
stopObservationOrTimer(instrumentation, request, response);
280-
return response;
281278
});
282279
}
283280

micrometer-core/src/test/java11/io/micrometer/core/instrument/binder/jdk/MicrometerHttpClientTests.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package io.micrometer.core.instrument.binder.jdk;
1717

18+
import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder;
19+
import com.github.tomakehurst.wiremock.http.Fault;
1820
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
1921
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
2022
import io.micrometer.core.instrument.MeterRegistry;
@@ -33,8 +35,10 @@
3335
import java.net.http.HttpRequest;
3436
import java.net.http.HttpResponse;
3537
import java.time.Duration;
38+
import java.util.concurrent.CompletionException;
3639

3740
import static com.github.tomakehurst.wiremock.client.WireMock.*;
41+
import static org.assertj.core.api.Assertions.*;
3842
import static org.assertj.core.api.BDDAssertions.then;
3943

4044
@WireMockTest
@@ -47,6 +51,8 @@ class MicrometerHttpClientTests {
4751
@BeforeEach
4852
void setup() {
4953
stubFor(any(urlEqualTo("/metrics")).willReturn(ok().withBody("body")));
54+
stubFor(any(urlEqualTo("/test-fault"))
55+
.willReturn(new ResponseDefinitionBuilder().withFault(Fault.CONNECTION_RESET_BY_PEER)));
5056
}
5157

5258
@Test
@@ -84,6 +90,43 @@ void shouldInstrumentHttpClientWithTimer(WireMockRuntimeInfo wmInfo) throws IOEx
8490
thenMeterRegistryContainsHttpClientTags();
8591
}
8692

93+
@Test
94+
void shouldThrowErrorFromSendAsync(WireMockRuntimeInfo wmInfo) {
95+
var client = MicrometerHttpClient.instrumentationBuilder(httpClient, meterRegistry).build();
96+
97+
String uri = "/test-fault";
98+
var request = HttpRequest.newBuilder(URI.create(wmInfo.getHttpBaseUrl() + uri))
99+
.header(MicrometerHttpClient.URI_PATTERN_HEADER, uri)
100+
.GET()
101+
.build();
102+
103+
var response = client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
104+
105+
assertThatThrownBy(response::join).isInstanceOf(CompletionException.class);
106+
assertThatNoException().isThrownBy(() -> meterRegistry.get("http.client.requests")
107+
.tag("method", "GET")
108+
.tag("uri", uri)
109+
.tag("status", "UNKNOWN")
110+
.tag("outcome", "UNKNOWN")
111+
.timer());
112+
}
113+
114+
@Test
115+
void sendAsyncShouldSetErrorInContext(WireMockRuntimeInfo wmInfo) {
116+
ObservationRegistry observationRegistry = TestObservationRegistry.create();
117+
StoreContextObservationHandler storeContextObservationHandler = new StoreContextObservationHandler();
118+
observationRegistry.observationConfig().observationHandler(storeContextObservationHandler);
119+
120+
var request = HttpRequest.newBuilder(URI.create(wmInfo.getHttpBaseUrl() + "/test-fault")).GET().build();
121+
122+
HttpClient observedClient = MicrometerHttpClient.instrumentationBuilder(httpClient, meterRegistry)
123+
.observationRegistry(observationRegistry)
124+
.build();
125+
var response = observedClient.sendAsync(request, HttpResponse.BodyHandlers.ofString());
126+
assertThatThrownBy(response::join).isInstanceOf(CompletionException.class);
127+
assertThat(storeContextObservationHandler.context.getError()).isInstanceOf(CompletionException.class);
128+
}
129+
87130
private void thenMeterRegistryContainsHttpClientTags() {
88131
then(meterRegistry.find("http.client.requests")
89132
.tag("method", "GET")
@@ -108,4 +151,20 @@ public void onStart(HttpClientContext context) {
108151
};
109152
}
110153

154+
static class StoreContextObservationHandler implements ObservationHandler<HttpClientContext> {
155+
156+
HttpClientContext context;
157+
158+
@Override
159+
public boolean supportsContext(Observation.Context context) {
160+
return context instanceof HttpClientContext;
161+
}
162+
163+
@Override
164+
public void onStart(HttpClientContext context) {
165+
this.context = context;
166+
}
167+
168+
}
169+
111170
}

0 commit comments

Comments
 (0)