Skip to content

Commit 93dd4c8

Browse files
authored
Propagate context into async http client CompletableFuture callbacks (#13041)
1 parent 44bea8d commit 93dd4c8

File tree

7 files changed

+152
-2
lines changed

7 files changed

+152
-2
lines changed

instrumentation/async-http-client/async-http-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientInstrumentationModule.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public List<TypeInstrumentation> typeInstrumentations() {
2323
return asList(
2424
new AsyncHttpClientInstrumentation(),
2525
new AsyncCompletionHandlerInstrumentation(),
26-
new NettyRequestSenderInstrumentation());
26+
new NettyRequestSenderInstrumentation(),
27+
new NettyResponseFutureInstrumentation());
2728
}
2829
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.asynchttpclient.v2_0;
7+
8+
import io.opentelemetry.context.Context;
9+
import io.opentelemetry.context.Scope;
10+
import java.util.concurrent.CompletableFuture;
11+
12+
public final class CompletableFutureWrapper {
13+
14+
private CompletableFutureWrapper() {}
15+
16+
public static <T> CompletableFuture<T> wrap(CompletableFuture<T> future, Context context) {
17+
CompletableFuture<T> result = new CompletableFuture<>();
18+
future.whenComplete(
19+
(T value, Throwable throwable) -> {
20+
try (Scope ignored = context.makeCurrent()) {
21+
if (throwable != null) {
22+
result.completeExceptionally(throwable);
23+
} else {
24+
result.complete(value);
25+
}
26+
}
27+
});
28+
29+
return result;
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.asynchttpclient.v2_0;
7+
8+
import static net.bytebuddy.matcher.ElementMatchers.named;
9+
import static net.bytebuddy.matcher.ElementMatchers.returns;
10+
import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments;
11+
12+
import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge;
13+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
14+
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
15+
import java.util.concurrent.CompletableFuture;
16+
import net.bytebuddy.asm.Advice;
17+
import net.bytebuddy.description.type.TypeDescription;
18+
import net.bytebuddy.matcher.ElementMatcher;
19+
20+
public class NettyResponseFutureInstrumentation implements TypeInstrumentation {
21+
22+
@Override
23+
public ElementMatcher<TypeDescription> typeMatcher() {
24+
return named("org.asynchttpclient.netty.NettyResponseFuture");
25+
}
26+
27+
@Override
28+
public void transform(TypeTransformer transformer) {
29+
transformer.applyAdviceToMethod(
30+
named("toCompletableFuture").and(takesNoArguments()).and(returns(CompletableFuture.class)),
31+
this.getClass().getName() + "$WrapFutureAdvice");
32+
}
33+
34+
@SuppressWarnings("unused")
35+
public static class WrapFutureAdvice {
36+
37+
@Advice.OnMethodExit(suppress = Throwable.class)
38+
public static void onExit(@Advice.Return(readOnly = false) CompletableFuture<?> result) {
39+
result = CompletableFutureWrapper.wrap(result, Java8BytecodeBridge.currentContext());
40+
}
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.asynchttpclient.v2_0;
7+
8+
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult;
9+
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions;
10+
import java.net.URI;
11+
import java.util.Map;
12+
import org.asynchttpclient.Request;
13+
14+
class AsyncHttpClientCompletableFutureTest extends AsyncHttpClientTest {
15+
16+
@Override
17+
protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
18+
super.configure(optionsBuilder);
19+
20+
optionsBuilder.setHasSendRequest(false);
21+
}
22+
23+
@Override
24+
public int sendRequest(Request request, String method, URI uri, Map<String, String> headers) {
25+
throw new IllegalStateException("this test only tests requests with callback");
26+
}
27+
28+
@Override
29+
public void sendRequestWithCallback(
30+
Request request,
31+
String method,
32+
URI uri,
33+
Map<String, String> headers,
34+
HttpClientResult requestResult) {
35+
client
36+
.executeRequest(request)
37+
.toCompletableFuture()
38+
.whenComplete(
39+
(response, throwable) -> {
40+
if (throwable == null) {
41+
requestResult.complete(response.getStatusCode());
42+
} else {
43+
requestResult.complete(throwable);
44+
}
45+
});
46+
}
47+
}

instrumentation/async-http-client/async-http-client-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/asynchttpclient/v2_0/AsyncHttpClientTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class AsyncHttpClientTest extends AbstractHttpClientTest<Request> {
3434
private static final int CONNECTION_TIMEOUT_MS = (int) CONNECTION_TIMEOUT.toMillis();
3535

3636
// request timeout is needed in addition to connect timeout on async-http-client versions 2.1.0+
37-
private static final AsyncHttpClient client = Dsl.asyncHttpClient(configureTimeout(Dsl.config()));
37+
static final AsyncHttpClient client = Dsl.asyncHttpClient(configureTimeout(Dsl.config()));
3838

3939
private static DefaultAsyncHttpClientConfig.Builder configureTimeout(
4040
DefaultAsyncHttpClientConfig.Builder builder) {

testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpClientTest.java

+24
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ void verifyExtension() {
111111
@ParameterizedTest
112112
@ValueSource(strings = {"/success", "/success?with=params"})
113113
void successfulGetRequest(String path) throws Exception {
114+
assumeTrue(options.getHasSendRequest());
115+
114116
URI uri = resolveAddress(path);
115117
String method = "GET";
116118
int responseCode = doRequest(method, uri);
@@ -130,6 +132,7 @@ void successfulGetRequest(String path) throws Exception {
130132
@Test
131133
void requestWithNonStandardHttpMethod() throws Exception {
132134
assumeTrue(options.getTestNonStandardHttpMethod());
135+
assumeTrue(options.getHasSendRequest());
133136

134137
URI uri = resolveAddress("/success");
135138
String method = "TEST";
@@ -150,6 +153,8 @@ void requestWithNonStandardHttpMethod() throws Exception {
150153
@ParameterizedTest
151154
@ValueSource(strings = {"PUT", "POST"})
152155
void successfulRequestWithParent(String method) throws Exception {
156+
assumeTrue(options.getHasSendRequest());
157+
153158
URI uri = resolveAddress("/success");
154159
int responseCode = testing.runWithSpan("parent", () -> doRequest(method, uri));
155160

@@ -168,6 +173,8 @@ void successfulRequestWithParent(String method) throws Exception {
168173

169174
@Test
170175
void successfulRequestWithNotSampledParent() throws Exception {
176+
assumeTrue(options.getHasSendRequest());
177+
171178
String method = "GET";
172179
URI uri = resolveAddress("/success");
173180
int responseCode = testing.runWithNonRecordingSpan(() -> doRequest(method, uri));
@@ -185,6 +192,7 @@ void successfulRequestWithNotSampledParent() throws Exception {
185192
void shouldSuppressNestedClientSpanIfAlreadyUnderParentClientSpan(String method)
186193
throws Exception {
187194
assumeTrue(options.getTestWithClientParent());
195+
assumeTrue(options.getHasSendRequest());
188196

189197
URI uri = resolveAddress("/success");
190198
int responseCode =
@@ -441,6 +449,8 @@ void redirectToSecuredCopiesAuthHeader() throws Exception {
441449
@ParameterizedTest
442450
@CsvSource({"/error,500", "/client-error,400"})
443451
void errorSpan(String path, int responseCode) {
452+
assumeTrue(options.getHasSendRequest());
453+
444454
String method = "GET";
445455
URI uri = resolveAddress(path);
446456

@@ -468,6 +478,7 @@ void errorSpan(String path, int responseCode) {
468478
@Test
469479
void reuseRequest() throws Exception {
470480
assumeTrue(options.getTestReusedRequest());
481+
assumeTrue(options.getHasSendRequest());
471482

472483
String method = "GET";
473484
URI uri = resolveAddress("/success");
@@ -497,6 +508,8 @@ void reuseRequest() throws Exception {
497508
// and the trace is not broken)
498509
@Test
499510
void requestWithExistingTracingHeaders() throws Exception {
511+
assumeTrue(options.getHasSendRequest());
512+
500513
String method = "GET";
501514
URI uri = resolveAddress("/success");
502515

@@ -515,6 +528,8 @@ void requestWithExistingTracingHeaders() throws Exception {
515528
@Test
516529
void captureHttpHeaders() throws Exception {
517530
assumeTrue(options.getTestCaptureHttpHeaders());
531+
assumeTrue(options.getHasSendRequest());
532+
518533
URI uri = resolveAddress("/success");
519534
String method = "GET";
520535
int responseCode =
@@ -544,6 +559,7 @@ void captureHttpHeaders() throws Exception {
544559
@Test
545560
void connectionErrorUnopenedPort() {
546561
assumeTrue(options.getTestConnectionFailure());
562+
assumeTrue(options.getHasSendRequest());
547563

548564
String method = "GET";
549565
URI uri = URI.create("http://localhost:" + PortUtils.UNUSABLE_PORT + '/');
@@ -615,6 +631,7 @@ void connectionErrorUnopenedPortWithCallback() throws Exception {
615631
@Test
616632
void connectionErrorNonRoutableAddress() {
617633
assumeTrue(options.getTestRemoteConnection());
634+
assumeTrue(options.getHasSendRequest());
618635

619636
String method = "HEAD";
620637
URI uri = URI.create(options.getTestHttps() ? "https://192.0.2.1/" : "http://192.0.2.1/");
@@ -648,6 +665,7 @@ void connectionErrorNonRoutableAddress() {
648665
@Test
649666
void readTimedOut() {
650667
assumeTrue(options.getTestReadTimeout());
668+
assumeTrue(options.getHasSendRequest());
651669

652670
String method = "GET";
653671
URI uri = resolveAddress("/read-timeout");
@@ -687,6 +705,7 @@ void readTimedOut() {
687705
void httpsRequest() throws Exception {
688706
assumeTrue(options.getTestRemoteConnection());
689707
assumeTrue(options.getTestHttps());
708+
assumeTrue(options.getHasSendRequest());
690709

691710
String method = "GET";
692711
URI uri = URI.create("https://localhost:" + server.httpsPort() + "/success");
@@ -705,6 +724,8 @@ void httpsRequest() throws Exception {
705724

706725
@Test
707726
void httpClientMetrics() throws Exception {
727+
assumeTrue(options.getHasSendRequest());
728+
708729
URI uri = resolveAddress("/success");
709730
String method = "GET";
710731
int responseCode = doRequest(method, uri);
@@ -743,6 +764,8 @@ void httpClientMetrics() throws Exception {
743764
*/
744765
@Test
745766
void highConcurrency() {
767+
assumeTrue(options.getHasSendRequest());
768+
746769
int count = 50;
747770
String method = "GET";
748771
URI uri = resolveAddress("/success");
@@ -968,6 +991,7 @@ void highConcurrencyOnSingleConnection() {
968991
@Test
969992
void spanEndsAfterBodyReceived() throws Exception {
970993
assumeTrue(options.isSpanEndsAfterBody());
994+
assumeTrue(options.getHasSendRequest());
971995

972996
String method = "GET";
973997
URI uri = resolveAddress("/long-request");

testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientTestOptions.java

+5
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ public boolean isLowLevelInstrumentation() {
9191

9292
public abstract boolean getTestCaptureHttpHeaders();
9393

94+
public abstract boolean getHasSendRequest();
95+
9496
public abstract Function<URI, String> getHttpProtocolVersion();
9597

9698
@Nullable
@@ -134,6 +136,7 @@ default Builder withDefaults() {
134136
.setTestErrorWithCallback(true)
135137
.setTestNonStandardHttpMethod(true)
136138
.setTestCaptureHttpHeaders(true)
139+
.setHasSendRequest(true)
137140
.setHttpProtocolVersion(uri -> "1.1");
138141
}
139142

@@ -179,6 +182,8 @@ default Builder withDefaults() {
179182

180183
Builder setTestNonStandardHttpMethod(boolean value);
181184

185+
Builder setHasSendRequest(boolean value);
186+
182187
Builder setHttpProtocolVersion(Function<URI, String> value);
183188

184189
@CanIgnoreReturnValue

0 commit comments

Comments
 (0)