Skip to content

Commit 57e57b9

Browse files
committed
Propagate Smallrye Context when switching REST Client context
The problem is that Opentelemetry creates the span context in MDCEnabledContextStorage when there is no VertxContext yet. Then, the span context of MDCEnabledContextStorage is propagated to the VertxContext in every operator of an Uni/Multi ... action. Before the changes in #32852, this was done in 2725325#diff-03d3c6adcdcc01b3a1f8daccc773adc6ee1c598afa245a445a24383a77f3b3a4L92. After the changes of #32852, we're switching the context but without using any Uni/Multi/... instance, so the span context is not attached to the one that will be created later in the VertxContext, and this is why we got two spans. Personally, I really dislike that the spancontext is being propagated only using the Uni/Multi/... instances (or at least, I could not find any other way to do it). I think we should implement a listener interface when switching of contexts (tho MDCEnabledContextStorage is an opentelemetry thing). To mimic what Smallrye does internally when calling the Uni/Multi operators, I directly call the Infrastructure.decorate method which fixes the issue. Fix #34212 Relates #32852
1 parent 6c169af commit 57e57b9

File tree

6 files changed

+157
-46
lines changed

6 files changed

+157
-46
lines changed

independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSwitchToRequestContextRestHandler.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext;
77
import org.jboss.resteasy.reactive.client.spi.ClientRestHandler;
88

9+
import io.smallrye.mutiny.infrastructure.Infrastructure;
910
import io.vertx.core.Context;
1011
import io.vertx.core.Handler;
1112
import io.vertx.core.Vertx;
@@ -29,10 +30,12 @@ public void handle(RestClientRequestContext requestContext) throws Exception {
2930
requestContext.resume(new Executor() {
3031
@Override
3132
public void execute(Runnable command) {
33+
// This is necessary to propagate the Smallrye context which is required for OpenTelemetry
34+
Runnable decorated = Infrastructure.decorate(command);
3235
captured.runOnContext(new Handler<Void>() {
3336
@Override
3437
public void handle(Void unused) {
35-
command.run();
38+
decorated.run();
3639
}
3740
});
3841
}

integration-tests/opentelemetry-reactive/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@
5757
<artifactId>awaitility</artifactId>
5858
<scope>test</scope>
5959
</dependency>
60+
<dependency>
61+
<groupId>com.github.tomakehurst</groupId>
62+
<artifactId>wiremock-jre8-standalone</artifactId>
63+
<scope>test</scope>
64+
</dependency>
6065

6166
<!-- Minimal test dependencies to *-deployment artifacts for consistent build order -->
6267
<dependency>

integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/OpenTelemetryReactiveClientTest.java

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_ROUTE;
88
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_STATUS_CODE;
99
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_TARGET;
10+
import static io.quarkus.it.opentelemetry.reactive.Utils.getSpanByKindAndParentId;
11+
import static io.quarkus.it.opentelemetry.reactive.Utils.getSpans;
1012
import static io.restassured.RestAssured.given;
11-
import static io.restassured.RestAssured.when;
1213
import static java.net.HttpURLConnection.HTTP_OK;
13-
import static java.util.stream.Collectors.toList;
1414
import static java.util.stream.Collectors.toSet;
1515
import static org.awaitility.Awaitility.await;
1616
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -28,7 +28,6 @@
2828

2929
import io.opentelemetry.api.trace.SpanKind;
3030
import io.quarkus.test.junit.QuarkusTest;
31-
import io.restassured.common.mapper.TypeRef;
3231
import io.smallrye.mutiny.Uni;
3332
import io.vertx.core.http.HttpMethod;
3433

@@ -113,23 +112,4 @@ void post() {
113112
assertEquals("helloPost", internal.get("name"));
114113
assertEquals(internal.get("parentSpanId"), server.get("spanId"));
115114
}
116-
117-
private static List<Map<String, Object>> getSpans() {
118-
return when().get("/export").body().as(new TypeRef<>() {
119-
});
120-
}
121-
122-
private static Map<String, Object> getSpanByKindAndParentId(List<Map<String, Object>> spans, SpanKind kind,
123-
Object parentSpanId) {
124-
List<Map<String, Object>> span = getSpansByKindAndParentId(spans, kind, parentSpanId);
125-
assertEquals(1, span.size());
126-
return span.get(0);
127-
}
128-
129-
private static List<Map<String, Object>> getSpansByKindAndParentId(List<Map<String, Object>> spans, SpanKind kind,
130-
Object parentSpanId) {
131-
return spans.stream()
132-
.filter(map -> map.get("kind").equals(kind.toString()))
133-
.filter(map -> map.get("parentSpanId").equals(parentSpanId)).collect(toList());
134-
}
135115
}

integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/OpenTelemetryReactiveTest.java

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
import static io.opentelemetry.api.trace.SpanKind.SERVER;
66
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_TARGET;
77
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_URL;
8+
import static io.quarkus.it.opentelemetry.reactive.Utils.getSpanByKindAndParentId;
9+
import static io.quarkus.it.opentelemetry.reactive.Utils.getSpans;
10+
import static io.quarkus.it.opentelemetry.reactive.Utils.getSpansByKindAndParentId;
811
import static io.restassured.RestAssured.given;
9-
import static io.restassured.RestAssured.when;
1012
import static java.net.HttpURLConnection.HTTP_OK;
11-
import static java.util.stream.Collectors.toList;
1213
import static java.util.stream.Collectors.toSet;
1314
import static org.awaitility.Awaitility.await;
1415
import static org.hamcrest.CoreMatchers.equalTo;
@@ -24,9 +25,7 @@
2425
import org.junit.jupiter.api.BeforeEach;
2526
import org.junit.jupiter.api.Test;
2627

27-
import io.opentelemetry.api.trace.SpanKind;
2828
import io.quarkus.test.junit.QuarkusTest;
29-
import io.restassured.common.mapper.TypeRef;
3029

3130
@QuarkusTest
3231
public class OpenTelemetryReactiveTest {
@@ -167,23 +166,4 @@ void multipleUsingCombine() {
167166
Map<String, Object> gokuInternal = getSpanByKindAndParentId(spans, INTERNAL, gokuServer.get("spanId"));
168167
assertEquals("helloGet", gokuInternal.get("name"));
169168
}
170-
171-
private static List<Map<String, Object>> getSpans() {
172-
return when().get("/export").body().as(new TypeRef<>() {
173-
});
174-
}
175-
176-
private static Map<String, Object> getSpanByKindAndParentId(List<Map<String, Object>> spans, SpanKind kind,
177-
Object parentSpanId) {
178-
List<Map<String, Object>> span = getSpansByKindAndParentId(spans, kind, parentSpanId);
179-
assertEquals(1, span.size());
180-
return span.get(0);
181-
}
182-
183-
private static List<Map<String, Object>> getSpansByKindAndParentId(List<Map<String, Object>> spans, SpanKind kind,
184-
Object parentSpanId) {
185-
return spans.stream()
186-
.filter(map -> map.get("kind").equals(kind.toString()))
187-
.filter(map -> map.get("parentSpanId").equals(parentSpanId)).collect(toList());
188-
}
189169
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package io.quarkus.it.opentelemetry.reactive;
2+
3+
import static com.github.tomakehurst.wiremock.client.WireMock.ok;
4+
import static io.opentelemetry.api.trace.SpanKind.CLIENT;
5+
import static io.opentelemetry.api.trace.SpanKind.INTERNAL;
6+
import static io.quarkus.it.opentelemetry.reactive.Utils.getSpanByKindAndParentId;
7+
import static io.quarkus.it.opentelemetry.reactive.Utils.getSpans;
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
10+
import java.net.URI;
11+
import java.util.List;
12+
import java.util.Map;
13+
14+
import jakarta.annotation.PostConstruct;
15+
import jakarta.enterprise.context.ApplicationScoped;
16+
import jakarta.ws.rs.GET;
17+
import jakarta.ws.rs.Path;
18+
19+
import org.eclipse.microprofile.config.inject.ConfigProperty;
20+
import org.eclipse.microprofile.rest.client.RestClientBuilder;
21+
import org.junit.jupiter.api.Test;
22+
23+
import com.github.tomakehurst.wiremock.WireMockServer;
24+
import com.github.tomakehurst.wiremock.client.WireMock;
25+
26+
import io.opentelemetry.instrumentation.annotations.WithSpan;
27+
import io.quarkus.runtime.Startup;
28+
import io.quarkus.test.common.QuarkusTestResource;
29+
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
30+
import io.quarkus.test.junit.QuarkusTest;
31+
32+
@QuarkusTestResource(restrictToAnnotatedClass = true, value = OpenTelemetryWithSpanAtStartupTest.MyWireMockResource.class)
33+
@QuarkusTest
34+
public class OpenTelemetryWithSpanAtStartupTest {
35+
36+
private static final int WIREMOCK_PORT = 20001;
37+
private static final String STARTUP_BEAN_ENABLED_PROPERTY = "startup.bean.enabled";
38+
39+
@Test
40+
void testGeneratedSpansUsingRestClientReactive() {
41+
List<Map<String, Object>> spans = getSpans();
42+
assertEquals(2, spans.size());
43+
44+
// First span is the callWireMockClient method. It does not have a parent span.
45+
Map<String, Object> client = getSpanByKindAndParentId(spans, INTERNAL, "0000000000000000");
46+
assertEquals("StartupBean.callWireMockClient", client.get("name"));
47+
48+
// We should get one client span, from the internal method.
49+
Map<String, Object> server = getSpanByKindAndParentId(spans, CLIENT, client.get("spanId"));
50+
assertEquals("GET", server.get("name"));
51+
}
52+
53+
@Startup
54+
@ApplicationScoped
55+
public static class StartupBean {
56+
57+
@ConfigProperty(name = STARTUP_BEAN_ENABLED_PROPERTY, defaultValue = "false")
58+
boolean enabled;
59+
60+
@PostConstruct
61+
void onStart() {
62+
if (enabled) {
63+
callWireMockClient();
64+
}
65+
}
66+
67+
@WithSpan
68+
public void callWireMockClient() {
69+
RestClientBuilder.newBuilder()
70+
.baseUri(URI.create("http://localhost:" + WIREMOCK_PORT))
71+
.build(WireMockRestClient.class)
72+
.call();
73+
}
74+
}
75+
76+
@Path("/stub")
77+
public interface WireMockRestClient {
78+
79+
@GET
80+
void call();
81+
}
82+
83+
public static class MyWireMockResource implements QuarkusTestResourceLifecycleManager {
84+
85+
WireMockServer wireMockServer;
86+
87+
@Override
88+
public Map<String, String> start() {
89+
wireMockServer = new WireMockServer(WIREMOCK_PORT);
90+
wireMockServer.stubFor(
91+
WireMock.get(WireMock.urlMatching("/stub"))
92+
.willReturn(ok()));
93+
wireMockServer.start();
94+
95+
return Map.of(STARTUP_BEAN_ENABLED_PROPERTY, Boolean.TRUE.toString());
96+
}
97+
98+
@Override
99+
public synchronized void stop() {
100+
if (wireMockServer != null) {
101+
wireMockServer.stop();
102+
wireMockServer = null;
103+
}
104+
}
105+
}
106+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package io.quarkus.it.opentelemetry.reactive;
2+
3+
import static io.restassured.RestAssured.when;
4+
import static java.util.stream.Collectors.toList;
5+
import static org.junit.jupiter.api.Assertions.assertEquals;
6+
7+
import java.util.List;
8+
import java.util.Map;
9+
10+
import io.opentelemetry.api.trace.SpanKind;
11+
import io.restassured.common.mapper.TypeRef;
12+
13+
public final class Utils {
14+
15+
private Utils() {
16+
17+
}
18+
19+
public static List<Map<String, Object>> getSpans() {
20+
return when().get("/export").body().as(new TypeRef<>() {
21+
});
22+
}
23+
24+
public static Map<String, Object> getSpanByKindAndParentId(List<Map<String, Object>> spans, SpanKind kind,
25+
Object parentSpanId) {
26+
List<Map<String, Object>> span = getSpansByKindAndParentId(spans, kind, parentSpanId);
27+
assertEquals(1, span.size());
28+
return span.get(0);
29+
}
30+
31+
public static List<Map<String, Object>> getSpansByKindAndParentId(List<Map<String, Object>> spans, SpanKind kind,
32+
Object parentSpanId) {
33+
return spans.stream()
34+
.filter(map -> map.get("kind").equals(kind.toString()))
35+
.filter(map -> map.get("parentSpanId").equals(parentSpanId)).collect(toList());
36+
}
37+
}

0 commit comments

Comments
 (0)