From efd326e6f9c6c3afab6ed05bd5ec3c3c2ea59408 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 28 May 2026 14:00:00 -0400 Subject: [PATCH] =?UTF-8?q?Add=20dd-java-agent=20instrumentation=20for=20q?= =?UTF-8?q?uarkus-rest-client-reactive=20(javax,=20MicroProfile=20REST=20C?= =?UTF-8?q?lient=201.0=E2=80=932.x)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the jakarta-namespace module (PR #11495) for Quarkus 2.x stacks that use the javax.ws.rs namespace. Intercepts RestClientBuilder.build(Class) and registers a ClientRequestFilter to inject Datadog + W3C propagation headers on every outbound REST call. Muzzle range [1.0, 3.0) ensures this module activates only when microprofile-rest-client-api < 3.0 is present; the jakarta module (#11495) handles 3.0+. Co-Authored-By: Claude Sonnet 4.6 --- .../build.gradle | 33 +++++++ .../InjectAdapter.java | 17 ++++ .../QuarkusRestClientJavaxDecorator.java | 56 +++++++++++ ...QuarkusRestClientJavaxInstrumentation.java | 57 +++++++++++ .../QuarkusRestClientJavaxTracingFilter.java | 48 +++++++++ ...kusRestClientJavaxInstrumentationTest.java | 97 +++++++++++++++++++ settings.gradle.kts | 1 + 7 files changed, 309 insertions(+) create mode 100644 dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/build.gradle create mode 100644 dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive_javax/InjectAdapter.java create mode 100644 dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive_javax/QuarkusRestClientJavaxDecorator.java create mode 100644 dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive_javax/QuarkusRestClientJavaxInstrumentation.java create mode 100644 dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive_javax/QuarkusRestClientJavaxTracingFilter.java create mode 100644 dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/src/test/java/datadog/trace/instrumentation/quarkus_rest_client_reactive_javax/QuarkusRestClientJavaxInstrumentationTest.java diff --git a/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/build.gradle b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/build.gradle new file mode 100644 index 00000000000..a14c63a8c19 --- /dev/null +++ b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/build.gradle @@ -0,0 +1,33 @@ +muzzle { + pass { + group = "org.eclipse.microprofile.rest.client" + module = "microprofile-rest-client-api" + versions = "[1.0, 3.0)" + assertInverse = true + } +} + +apply from: "$rootDir/gradle/java.gradle" + +addTestSuiteForDir('latestDepTest', 'test') + +dependencies { + compileOnly group: 'org.eclipse.microprofile.rest.client', name: 'microprofile-rest-client-api', version: '2.0' + compileOnly group: 'javax.ws.rs', name: 'javax.ws.rs-api', version: '2.1.1' + compileOnly group: 'javax.annotation', name: 'javax.annotation-api', version: '1.3.2' + + testImplementation group: 'org.eclipse.microprofile.rest.client', name: 'microprofile-rest-client-api', version: '2.0' + testImplementation group: 'javax.ws.rs', name: 'javax.ws.rs-api', version: '2.1.1' + testImplementation group: 'javax.annotation', name: 'javax.annotation-api', version: '1.3.2' + // RESTEasy Microprofile client 1.x uses javax namespace and works standalone without Quarkus. + testImplementation group: 'org.jboss.resteasy.microprofile', name: 'microprofile-rest-client', version: '1.0.0.Final' + testImplementation group: 'org.jboss.resteasy', name: 'resteasy-client', version: '4.7.9.Final' + testImplementation group: 'org.jboss.resteasy', name: 'resteasy-jackson2-provider', version: '4.7.9.Final' + // MicroProfile Config implementation required by RESTEasy MicroProfile exception mapper at runtime. + testImplementation group: 'io.smallrye.config', name: 'smallrye-config', version: '2.13.3' + + latestDepTestImplementation group: 'org.eclipse.microprofile.rest.client', name: 'microprofile-rest-client-api', version: '2.+' + latestDepTestImplementation group: 'org.jboss.resteasy.microprofile', name: 'microprofile-rest-client', version: '1.+' + latestDepTestImplementation group: 'org.jboss.resteasy', name: 'resteasy-client', version: '4.+' + latestDepTestImplementation group: 'org.jboss.resteasy', name: 'resteasy-jackson2-provider', version: '4.+' +} diff --git a/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive_javax/InjectAdapter.java b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive_javax/InjectAdapter.java new file mode 100644 index 00000000000..49eea718cc1 --- /dev/null +++ b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive_javax/InjectAdapter.java @@ -0,0 +1,17 @@ +package datadog.trace.instrumentation.quarkus_rest_client_reactive_javax; + +import datadog.context.propagation.CarrierSetter; +import javax.annotation.ParametersAreNonnullByDefault; +import javax.ws.rs.core.MultivaluedMap; + +@ParametersAreNonnullByDefault +public final class InjectAdapter implements CarrierSetter> { + + public static final InjectAdapter SETTER = new InjectAdapter(); + + @Override + public void set( + final MultivaluedMap headers, final String key, final String value) { + headers.putSingle(key, value); + } +} diff --git a/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive_javax/QuarkusRestClientJavaxDecorator.java b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive_javax/QuarkusRestClientJavaxDecorator.java new file mode 100644 index 00000000000..83253efea0d --- /dev/null +++ b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive_javax/QuarkusRestClientJavaxDecorator.java @@ -0,0 +1,56 @@ +package datadog.trace.instrumentation.quarkus_rest_client_reactive_javax; + +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import datadog.trace.bootstrap.instrumentation.decorator.HttpClientDecorator; +import java.net.URI; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientResponseContext; + +public class QuarkusRestClientJavaxDecorator + extends HttpClientDecorator { + + public static final CharSequence QUARKUS_REST_CLIENT = + UTF8BytesString.create("quarkus-rest-client-reactive"); + public static final QuarkusRestClientJavaxDecorator DECORATE = + new QuarkusRestClientJavaxDecorator(); + public static final CharSequence QUARKUS_REST_CLIENT_CALL = + UTF8BytesString.create(DECORATE.operationName()); + + @Override + protected String[] instrumentationNames() { + return new String[] { + "quarkus-rest-client-reactive", "quarkus-rest-client", "microprofile-rest-client" + }; + } + + @Override + protected CharSequence component() { + return QUARKUS_REST_CLIENT; + } + + @Override + protected String method(final ClientRequestContext request) { + return request.getMethod(); + } + + @Override + protected URI url(final ClientRequestContext request) { + return request.getUri(); + } + + @Override + protected int status(final ClientResponseContext response) { + return response.getStatus(); + } + + @Override + protected String getRequestHeader(final ClientRequestContext request, final String headerName) { + return request.getHeaderString(headerName); + } + + @Override + protected String getResponseHeader( + final ClientResponseContext response, final String headerName) { + return response.getHeaderString(headerName); + } +} diff --git a/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive_javax/QuarkusRestClientJavaxInstrumentation.java b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive_javax/QuarkusRestClientJavaxInstrumentation.java new file mode 100644 index 00000000000..78a55575224 --- /dev/null +++ b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive_javax/QuarkusRestClientJavaxInstrumentation.java @@ -0,0 +1,57 @@ +package datadog.trace.instrumentation.quarkus_rest_client_reactive_javax; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.implementsInterface; +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.eclipse.microprofile.rest.client.RestClientBuilder; + +@AutoService(InstrumenterModule.class) +public final class QuarkusRestClientJavaxInstrumentation extends InstrumenterModule.Tracing + implements Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice { + + public QuarkusRestClientJavaxInstrumentation() { + super("quarkus-rest-client-reactive", "quarkus-rest-client", "microprofile-rest-client"); + } + + @Override + public String hierarchyMarkerType() { + return "org.eclipse.microprofile.rest.client.RestClientBuilder"; + } + + @Override + public ElementMatcher hierarchyMatcher() { + return implementsInterface(named(hierarchyMarkerType())); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".QuarkusRestClientJavaxDecorator", + packageName + ".QuarkusRestClientJavaxTracingFilter", + packageName + ".InjectAdapter", + }; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isMethod().and(named("build")).and(takesArgument(0, Class.class)), + QuarkusRestClientJavaxInstrumentation.class.getName() + "$RestClientBuilderAdvice"); + } + + public static class RestClientBuilderAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void registerFilter(@Advice.This final RestClientBuilder builder) { + builder.register(QuarkusRestClientJavaxTracingFilter.class); + } + } +} diff --git a/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive_javax/QuarkusRestClientJavaxTracingFilter.java b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive_javax/QuarkusRestClientJavaxTracingFilter.java new file mode 100644 index 00000000000..5b359dccc8c --- /dev/null +++ b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive_javax/QuarkusRestClientJavaxTracingFilter.java @@ -0,0 +1,48 @@ +package datadog.trace.instrumentation.quarkus_rest_client_reactive_javax; + +import static datadog.context.Context.current; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan; +import static datadog.trace.instrumentation.quarkus_rest_client_reactive_javax.InjectAdapter.SETTER; +import static datadog.trace.instrumentation.quarkus_rest_client_reactive_javax.QuarkusRestClientJavaxDecorator.DECORATE; +import static datadog.trace.instrumentation.quarkus_rest_client_reactive_javax.QuarkusRestClientJavaxDecorator.QUARKUS_REST_CLIENT; +import static datadog.trace.instrumentation.quarkus_rest_client_reactive_javax.QuarkusRestClientJavaxDecorator.QUARKUS_REST_CLIENT_CALL; + +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import javax.annotation.Priority; +import javax.ws.rs.Priorities; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.client.ClientResponseFilter; + +@Priority(Priorities.HEADER_DECORATOR) +public class QuarkusRestClientJavaxTracingFilter + implements ClientRequestFilter, ClientResponseFilter { + + public static final String SPAN_PROPERTY_NAME = "datadog.trace.quarkus-rest-client.span"; + + @Override + public void filter(final ClientRequestContext requestContext) { + final AgentSpan span = startSpan(QUARKUS_REST_CLIENT.toString(), QUARKUS_REST_CLIENT_CALL); + try (final AgentScope scope = activateSpan(span)) { + DECORATE.afterStart(span); + DECORATE.onRequest(span, requestContext); + DECORATE.injectContext(current().with(span), requestContext.getHeaders(), SETTER); + requestContext.setProperty(SPAN_PROPERTY_NAME, span); + } + } + + @Override + public void filter( + final ClientRequestContext requestContext, final ClientResponseContext responseContext) { + final Object spanObj = requestContext.getProperty(SPAN_PROPERTY_NAME); + if (spanObj instanceof AgentSpan) { + final AgentSpan span = (AgentSpan) spanObj; + DECORATE.onResponse(span, responseContext); + DECORATE.beforeFinish(span); + span.finish(); + } + } +} diff --git a/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/src/test/java/datadog/trace/instrumentation/quarkus_rest_client_reactive_javax/QuarkusRestClientJavaxInstrumentationTest.java b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/src/test/java/datadog/trace/instrumentation/quarkus_rest_client_reactive_javax/QuarkusRestClientJavaxInstrumentationTest.java new file mode 100644 index 00000000000..2087b75ab11 --- /dev/null +++ b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-2.0/src/test/java/datadog/trace/instrumentation/quarkus_rest_client_reactive_javax/QuarkusRestClientJavaxInstrumentationTest.java @@ -0,0 +1,97 @@ +package datadog.trace.instrumentation.quarkus_rest_client_reactive_javax; + +import static datadog.trace.agent.test.assertions.SpanMatcher.span; +import static datadog.trace.agent.test.assertions.TraceMatcher.trace; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import datadog.trace.agent.test.AbstractInstrumentationTest; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.concurrent.atomic.AtomicReference; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class QuarkusRestClientJavaxInstrumentationTest extends AbstractInstrumentationTest { + + private static HttpServer server; + private static int port; + private static final AtomicReference capturedTraceId = new AtomicReference<>(); + private static final AtomicReference capturedParentId = new AtomicReference<>(); + + @RegisterRestClient + public interface HelloClient { + @GET + @Path("/hello") + Response hello(); + } + + @BeforeAll + static void startServer() throws IOException { + server = HttpServer.create(new InetSocketAddress(0), 0); + server.createContext( + "/hello", + new HttpHandler() { + @Override + public void handle(HttpExchange exchange) throws IOException { + capturedTraceId.set(exchange.getRequestHeaders().getFirst("x-datadog-trace-id")); + capturedParentId.set(exchange.getRequestHeaders().getFirst("x-datadog-parent-id")); + byte[] body = "hello".getBytes(); + exchange.sendResponseHeaders(200, body.length); + exchange.getResponseBody().write(body); + exchange.close(); + } + }); + server.start(); + port = server.getAddress().getPort(); + } + + @AfterAll + static void stopServer() { + server.stop(0); + } + + @Test + void propagationHeadersAreInjectedOnRestClientCall() throws Exception { + HelloClient client = + RestClientBuilder.newBuilder() + .baseUri(URI.create("http://localhost:" + port)) + .build(HelloClient.class); + + try (Response response = client.hello()) { + assertEquals(200, response.getStatus()); + } + + assertNotNull(capturedTraceId.get(), "x-datadog-trace-id header should be injected"); + assertNotNull(capturedParentId.get(), "x-datadog-parent-id header should be injected"); + + assertTraces(trace(span().type("http"))); + } + + @Test + void spanIsCreatedForEachRequest() throws Exception { + HelloClient client = + RestClientBuilder.newBuilder() + .baseUri(URI.create("http://localhost:" + port)) + .build(HelloClient.class); + + try (Response r1 = client.hello()) { + assertEquals(200, r1.getStatus()); + } + try (Response r2 = client.hello()) { + assertEquals(200, r2.getStatus()); + } + + assertTraces(trace(span().type("http")), trace(span().type("http"))); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index dda1f432e6f..e54f6364ea8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -530,6 +530,7 @@ include( ":dd-java-agent:instrumentation:play:play-appsec-2.7", ":dd-java-agent:instrumentation:play:play-appsec-common", ":dd-java-agent:instrumentation:protobuf-3.0", + ":dd-java-agent:instrumentation:quarkus:quarkus-rest-client-reactive-2.0", ":dd-java-agent:instrumentation:quartz-2.0", ":dd-java-agent:instrumentation:rabbitmq-amqp-2.7", ":dd-java-agent:instrumentation:ratpack-1.5",