Skip to content

Commit aaa5689

Browse files
authored
Merge pull request quarkusio#36123 from manovotn/issue36118_2
Make rest-client invocation context implement ArcInvocationContext
2 parents 1e3a64b + f35b854 commit aaa5689

File tree

4 files changed

+300
-10
lines changed

4 files changed

+300
-10
lines changed
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package io.quarkus.restclient.runtime;
2+
3+
import java.lang.annotation.Annotation;
4+
import java.lang.reflect.Constructor;
5+
import java.lang.reflect.InvocationTargetException;
6+
import java.lang.reflect.Method;
7+
import java.util.ArrayList;
8+
import java.util.Collections;
9+
import java.util.HashMap;
10+
import java.util.List;
11+
import java.util.Map;
12+
import java.util.Set;
13+
import java.util.concurrent.CompletionException;
14+
15+
import jakarta.enterprise.inject.spi.InterceptionType;
16+
import jakarta.enterprise.inject.spi.Interceptor;
17+
import jakarta.interceptor.InvocationContext;
18+
import jakarta.ws.rs.client.ResponseProcessingException;
19+
20+
import org.jboss.resteasy.microprofile.client.ExceptionMapping;
21+
22+
import io.quarkus.arc.ArcInvocationContext;
23+
24+
/**
25+
* A Quarkus copy of {@link org.jboss.resteasy.microprofile.client.InvocationContextImpl} which makes it implement
26+
* {@link ArcInvocationContext} instead so that it's compatible with Quarkus interceptors.
27+
*/
28+
public class QuarkusInvocationContextImpl implements ArcInvocationContext {
29+
30+
private final Object target;
31+
32+
private final Method method;
33+
34+
private Object[] args;
35+
36+
private final int position;
37+
38+
private final Map<String, Object> contextData;
39+
40+
private final List<QuarkusInvocationContextImpl.InterceptorInvocation> chain;
41+
42+
private final Set<Annotation> interceptorBindings;
43+
44+
public QuarkusInvocationContextImpl(final Object target, final Method method, final Object[] args,
45+
final List<QuarkusInvocationContextImpl.InterceptorInvocation> chain, Set<Annotation> interceptorBindings) {
46+
this(target, method, args, chain, 0, interceptorBindings);
47+
}
48+
49+
private QuarkusInvocationContextImpl(final Object target, final Method method, final Object[] args,
50+
final List<QuarkusInvocationContextImpl.InterceptorInvocation> chain, final int position,
51+
Set<Annotation> interceptorBindings) {
52+
this.target = target;
53+
this.method = method;
54+
this.args = args;
55+
this.interceptorBindings = interceptorBindings == null ? Collections.emptySet() : interceptorBindings;
56+
this.contextData = new HashMap<>();
57+
// put in bindings under Arc's specific key
58+
this.contextData.put(ArcInvocationContext.KEY_INTERCEPTOR_BINDINGS, interceptorBindings);
59+
this.position = position;
60+
this.chain = chain;
61+
}
62+
63+
boolean hasNextInterceptor() {
64+
return position < chain.size();
65+
}
66+
67+
protected Object invokeNext() throws Exception {
68+
return chain.get(position).invoke(nextContext());
69+
}
70+
71+
private InvocationContext nextContext() {
72+
return new QuarkusInvocationContextImpl(target, method, args, chain, position + 1, interceptorBindings);
73+
}
74+
75+
protected Object interceptorChainCompleted() throws Exception {
76+
try {
77+
return method.invoke(target, args);
78+
} catch (InvocationTargetException e) {
79+
Throwable cause = e.getCause();
80+
if (cause instanceof CompletionException) {
81+
cause = cause.getCause();
82+
}
83+
if (cause instanceof ExceptionMapping.HandlerException) {
84+
((ExceptionMapping.HandlerException) cause).mapException(method);
85+
}
86+
if (cause instanceof ResponseProcessingException) {
87+
ResponseProcessingException rpe = (ResponseProcessingException) cause;
88+
// Note that the default client engine leverages a single connection
89+
// MP FT: we need to close the response otherwise we would not be able to retry if the method returns jakarta.ws.rs.core.Response
90+
rpe.getResponse().close();
91+
cause = rpe.getCause();
92+
if (cause instanceof RuntimeException) {
93+
throw (RuntimeException) cause;
94+
}
95+
}
96+
throw e;
97+
}
98+
}
99+
100+
@Override
101+
public Object proceed() throws Exception {
102+
try {
103+
if (hasNextInterceptor()) {
104+
return invokeNext();
105+
} else {
106+
return interceptorChainCompleted();
107+
}
108+
} catch (InvocationTargetException e) {
109+
Throwable cause = e.getCause();
110+
if (cause instanceof Error) {
111+
throw (Error) cause;
112+
}
113+
if (cause instanceof Exception) {
114+
throw (Exception) cause;
115+
}
116+
throw new RuntimeException(cause);
117+
}
118+
}
119+
120+
@Override
121+
public Object getTarget() {
122+
return target;
123+
}
124+
125+
@Override
126+
public Method getMethod() {
127+
return method;
128+
}
129+
130+
@Override
131+
public Constructor<?> getConstructor() {
132+
return null;
133+
}
134+
135+
@Override
136+
public Object[] getParameters() throws IllegalStateException {
137+
return args;
138+
}
139+
140+
@Override
141+
public void setParameters(Object[] params) throws IllegalStateException, IllegalArgumentException {
142+
this.args = params;
143+
}
144+
145+
@Override
146+
public Map<String, Object> getContextData() {
147+
return contextData;
148+
}
149+
150+
@Override
151+
public Object getTimer() {
152+
return null;
153+
}
154+
155+
@Override
156+
public Set<Annotation> getInterceptorBindings() {
157+
return interceptorBindings;
158+
}
159+
160+
@Override
161+
public <T extends Annotation> T findIterceptorBinding(Class<T> annotationType) {
162+
for (Annotation annotation : getInterceptorBindings()) {
163+
if (annotation.annotationType().equals(annotationType)) {
164+
return (T) annotation;
165+
}
166+
}
167+
return null;
168+
}
169+
170+
@Override
171+
public <T extends Annotation> List<T> findIterceptorBindings(Class<T> annotationType) {
172+
List<T> found = new ArrayList<>();
173+
for (Annotation annotation : getInterceptorBindings()) {
174+
if (annotation.annotationType().equals(annotationType)) {
175+
found.add((T) annotation);
176+
}
177+
}
178+
return found;
179+
}
180+
181+
public static class InterceptorInvocation {
182+
183+
@SuppressWarnings("rawtypes")
184+
private final Interceptor interceptor;
185+
186+
private final Object interceptorInstance;
187+
188+
public InterceptorInvocation(final Interceptor<?> interceptor, final Object interceptorInstance) {
189+
this.interceptor = interceptor;
190+
this.interceptorInstance = interceptorInstance;
191+
}
192+
193+
@SuppressWarnings("unchecked")
194+
Object invoke(InvocationContext ctx) throws Exception {
195+
return interceptor.intercept(InterceptionType.AROUND_INVOKE, interceptorInstance, ctx);
196+
}
197+
}
198+
}

extensions/resteasy-classic/rest-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusProxyInvocationHandler.java

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
import org.jboss.logging.Logger;
3131
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
3232
import org.jboss.resteasy.microprofile.client.ExceptionMapping;
33-
import org.jboss.resteasy.microprofile.client.InvocationContextImpl;
3433
import org.jboss.resteasy.microprofile.client.RestClientProxy;
3534
import org.jboss.resteasy.microprofile.client.header.ClientHeaderFillingException;
3635

@@ -52,7 +51,9 @@ public class QuarkusProxyInvocationHandler implements InvocationHandler {
5251

5352
private final Set<Object> providerInstances;
5453

55-
private final Map<Method, List<InvocationContextImpl.InterceptorInvocation>> interceptorChains;
54+
private final Map<Method, List<QuarkusInvocationContextImpl.InterceptorInvocation>> interceptorChains;
55+
56+
private final Map<Method, Set<Annotation>> interceptorBindingsMap;
5657

5758
private final ResteasyClient client;
5859

@@ -70,10 +71,13 @@ public QuarkusProxyInvocationHandler(final Class<?> restClientInterface,
7071
this.closed = new AtomicBoolean();
7172
if (beanManager != null) {
7273
this.creationalContext = beanManager.createCreationalContext(null);
73-
this.interceptorChains = initInterceptorChains(beanManager, creationalContext, restClientInterface);
74+
this.interceptorBindingsMap = new HashMap<>();
75+
this.interceptorChains = initInterceptorChains(beanManager, creationalContext, restClientInterface,
76+
interceptorBindingsMap);
7477
} else {
7578
this.creationalContext = null;
7679
this.interceptorChains = Collections.emptyMap();
80+
this.interceptorBindingsMap = Collections.emptyMap();
7781
}
7882
}
7983

@@ -152,10 +156,10 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
152156
args = argsReplacement;
153157
}
154158

155-
List<InvocationContextImpl.InterceptorInvocation> chain = interceptorChains.get(method);
159+
List<QuarkusInvocationContextImpl.InterceptorInvocation> chain = interceptorChains.get(method);
156160
if (chain != null) {
157161
// Invoke business method interceptors
158-
return new InvocationContextImpl(target, method, args, chain).proceed();
162+
return new QuarkusInvocationContextImpl(target, method, args, chain, interceptorBindingsMap.get(method)).proceed();
159163
} else {
160164
try {
161165
return method.invoke(target, args);
@@ -245,10 +249,11 @@ private static BeanManager getBeanManager(Class<?> restClientInterface) {
245249
}
246250
}
247251

248-
private static Map<Method, List<InvocationContextImpl.InterceptorInvocation>> initInterceptorChains(
249-
BeanManager beanManager, CreationalContext<?> creationalContext, Class<?> restClientInterface) {
252+
private static Map<Method, List<QuarkusInvocationContextImpl.InterceptorInvocation>> initInterceptorChains(
253+
BeanManager beanManager, CreationalContext<?> creationalContext, Class<?> restClientInterface,
254+
Map<Method, Set<Annotation>> interceptorBindingsMap) {
250255

251-
Map<Method, List<InvocationContextImpl.InterceptorInvocation>> chains = new HashMap<>();
256+
Map<Method, List<QuarkusInvocationContextImpl.InterceptorInvocation>> chains = new HashMap<>();
252257
// Interceptor as a key in a map is not entirely correct (custom interceptors) but should work in most cases
253258
Map<Interceptor<?>, Object> interceptorInstances = new HashMap<>();
254259

@@ -267,12 +272,13 @@ private static Map<Method, List<InvocationContextImpl.InterceptorInvocation>> in
267272
List<Interceptor<?>> interceptors = beanManager.resolveInterceptors(InterceptionType.AROUND_INVOKE,
268273
interceptorBindings);
269274
if (!interceptors.isEmpty()) {
270-
List<InvocationContextImpl.InterceptorInvocation> chain = new ArrayList<>();
275+
List<QuarkusInvocationContextImpl.InterceptorInvocation> chain = new ArrayList<>();
271276
for (Interceptor<?> interceptor : interceptors) {
272-
chain.add(new InvocationContextImpl.InterceptorInvocation(interceptor,
277+
chain.add(new QuarkusInvocationContextImpl.InterceptorInvocation(interceptor,
273278
interceptorInstances.computeIfAbsent(interceptor,
274279
i -> beanManager.getReference(i, i.getBeanClass(), creationalContext))));
275280
}
281+
interceptorBindingsMap.put(method, Set.of(interceptorBindings));
276282
chains.put(method, chain);
277283
}
278284
}

integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/PingPongResource.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
1111
import org.eclipse.microprofile.rest.client.inject.RestClient;
1212

13+
import io.opentelemetry.instrumentation.annotations.SpanAttribute;
14+
import io.opentelemetry.instrumentation.annotations.WithSpan;
1315
import io.smallrye.common.annotation.Blocking;
1416
import io.smallrye.mutiny.Uni;
1517
import io.vertx.core.MultiMap;
@@ -34,6 +36,11 @@ public interface PingPongRestClient {
3436
@GET
3537
@Path("/client/pong/{message}")
3638
Uni<String> asyncPingpong(@PathParam("message") String message);
39+
40+
@GET
41+
@Path("/client/pong/{message}")
42+
@WithSpan
43+
String pingpongIntercept(@SpanAttribute(value = "message") @PathParam("message") String message);
3744
}
3845

3946
@Inject
@@ -81,4 +88,9 @@ public Uni<String> asyncPingNamed(@PathParam("message") String message) {
8188
.onItemOrFailure().call(httpClient::close);
8289
}
8390

91+
@GET
92+
@Path("pong-intercept/{message}")
93+
public String pongIntercept(@PathParam("message") String message) {
94+
return pingRestClient.pingpongIntercept(message);
95+
}
8496
}

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

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import static org.junit.jupiter.api.Assertions.assertEquals;
1616
import static org.junit.jupiter.api.Assertions.assertFalse;
1717
import static org.junit.jupiter.api.Assertions.assertNotNull;
18+
import static org.junit.jupiter.api.Assertions.assertNull;
1819
import static org.junit.jupiter.api.Assertions.assertTrue;
1920
import static org.junit.jupiter.api.Assertions.fail;
2021

@@ -557,6 +558,79 @@ void testAsyncClientTracing() {
557558
assertNotNull(clientServer.get("attr_user_agent.original"));
558559
}
559560

561+
@Test
562+
void testClientTracingWithInterceptor() {
563+
given()
564+
.when().get("/client/pong-intercept/one")
565+
.then()
566+
.statusCode(200)
567+
.body(containsString("one"));
568+
569+
await().atMost(5, SECONDS).until(() -> getSpans().size() == 4);
570+
List<Map<String, Object>> spans = getSpans();
571+
assertEquals(4, spans.size());
572+
assertEquals(1, spans.stream().map(map -> map.get("traceId")).collect(toSet()).size());
573+
574+
Map<String, Object> server = getSpanByKindAndParentId(spans, SERVER, "0000000000000000");
575+
assertEquals(SERVER.toString(), server.get("kind"));
576+
verifyResource(server);
577+
assertEquals("GET /client/pong-intercept/{message}", server.get("name"));
578+
assertEquals(SERVER.toString(), server.get("kind"));
579+
assertTrue((Boolean) server.get("ended"));
580+
assertEquals(SpanId.getInvalid(), server.get("parent_spanId"));
581+
assertEquals(TraceId.getInvalid(), server.get("parent_traceId"));
582+
assertFalse((Boolean) server.get("parent_valid"));
583+
assertFalse((Boolean) server.get("parent_remote"));
584+
assertEquals("GET", server.get("attr_http.method"));
585+
assertEquals("/client/pong-intercept/one", server.get("attr_http.target"));
586+
assertEquals(pathParamUrl.getHost(), server.get("attr_net.host.name"));
587+
assertEquals(pathParamUrl.getPort(), Integer.valueOf((String) server.get("attr_net.host.port")));
588+
assertEquals("http", server.get("attr_http.scheme"));
589+
assertEquals("/client/pong-intercept/{message}", server.get("attr_http.route"));
590+
assertEquals("200", server.get("attr_http.status_code"));
591+
assertNotNull(server.get("attr_http.client_ip"));
592+
assertNotNull(server.get("attr_user_agent.original"));
593+
594+
Map<String, Object> fromInterceptor = getSpanByKindAndParentId(spans, INTERNAL, server.get("spanId"));
595+
assertEquals("PingPongRestClient.pingpongIntercept", fromInterceptor.get("name"));
596+
assertEquals(INTERNAL.toString(), fromInterceptor.get("kind"));
597+
assertTrue((Boolean) fromInterceptor.get("ended"));
598+
assertTrue((Boolean) fromInterceptor.get("parent_valid"));
599+
assertFalse((Boolean) fromInterceptor.get("parent_remote"));
600+
assertNull(fromInterceptor.get("attr_http.method"));
601+
assertNull(fromInterceptor.get("attr_http.status_code"));
602+
assertEquals("one", fromInterceptor.get("attr_message"));
603+
604+
Map<String, Object> client = getSpanByKindAndParentId(spans, CLIENT, fromInterceptor.get("spanId"));
605+
assertEquals("GET", client.get("name"));
606+
assertEquals(SpanKind.CLIENT.toString(), client.get("kind"));
607+
assertTrue((Boolean) client.get("ended"));
608+
assertTrue((Boolean) client.get("parent_valid"));
609+
assertFalse((Boolean) client.get("parent_remote"));
610+
assertEquals("GET", client.get("attr_http.method"));
611+
assertEquals("http://localhost:8081/client/pong/one", client.get("attr_http.url"));
612+
assertEquals("200", client.get("attr_http.status_code"));
613+
614+
Map<String, Object> clientServer = getSpanByKindAndParentId(spans, SERVER, client.get("spanId"));
615+
assertEquals(SERVER.toString(), clientServer.get("kind"));
616+
verifyResource(clientServer);
617+
assertEquals("GET /client/pong/{message}", clientServer.get("name"));
618+
assertEquals(SERVER.toString(), clientServer.get("kind"));
619+
assertTrue((Boolean) clientServer.get("ended"));
620+
assertTrue((Boolean) clientServer.get("parent_valid"));
621+
assertTrue((Boolean) clientServer.get("parent_remote"));
622+
assertEquals("GET", clientServer.get("attr_http.method"));
623+
assertEquals("/client/pong/one", clientServer.get("attr_http.target"));
624+
assertEquals(pathParamUrl.getHost(), server.get("attr_net.host.name"));
625+
assertEquals(pathParamUrl.getPort(), Integer.valueOf((String) server.get("attr_net.host.port")));
626+
assertEquals("http", clientServer.get("attr_http.scheme"));
627+
assertEquals("/client/pong/{message}", clientServer.get("attr_http.route"));
628+
assertEquals("200", clientServer.get("attr_http.status_code"));
629+
assertNotNull(clientServer.get("attr_http.client_ip"));
630+
assertNotNull(clientServer.get("attr_user_agent.original"));
631+
assertEquals(clientServer.get("parentSpanId"), client.get("spanId"));
632+
}
633+
560634
@Test
561635
void testTemplatedPathOnClass() {
562636
given()

0 commit comments

Comments
 (0)