diff --git a/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-3.0/build.gradle b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-3.0/build.gradle new file mode 100644 index 00000000000..98d20fd9fb5 --- /dev/null +++ b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-3.0/build.gradle @@ -0,0 +1,33 @@ +muzzle { + pass { + group = "org.eclipse.microprofile.rest.client" + module = "microprofile-rest-client-api" + versions = "[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: '3.0' + compileOnly group: 'jakarta.ws.rs', name: 'jakarta.ws.rs-api', version: '3.0.0' + compileOnly group: 'jakarta.annotation', name: 'jakarta.annotation-api', version: '2.0.0' + + testImplementation group: 'org.eclipse.microprofile.rest.client', name: 'microprofile-rest-client-api', version: '3.0' + testImplementation group: 'jakarta.ws.rs', name: 'jakarta.ws.rs-api', version: '3.0.0' + testImplementation group: 'jakarta.annotation', name: 'jakarta.annotation-api', version: '2.0.0' + // RESTEasy Microprofile client 2.x uses jakarta namespace and works standalone without Quarkus. + testImplementation group: 'org.jboss.resteasy.microprofile', name: 'microprofile-rest-client', version: '2.0.0.Final' + testImplementation group: 'org.jboss.resteasy', name: 'resteasy-client', version: '6.0.0.Final' + testImplementation group: 'org.jboss.resteasy', name: 'resteasy-jackson2-provider', version: '6.0.0.Final' + // MicroProfile Config implementation required by RESTEasy MicroProfile exception mapper at runtime. + testImplementation group: 'io.smallrye.config', name: 'smallrye-config', version: '3.0.0' + + latestDepTestImplementation group: 'org.eclipse.microprofile.rest.client', name: 'microprofile-rest-client-api', version: '3.+' + latestDepTestImplementation group: 'org.jboss.resteasy.microprofile', name: 'microprofile-rest-client', version: '2.+' + latestDepTestImplementation group: 'org.jboss.resteasy', name: 'resteasy-client', version: '6.+' + latestDepTestImplementation group: 'org.jboss.resteasy', name: 'resteasy-jackson2-provider', version: '6.+' +} diff --git a/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-3.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive/InjectAdapter.java b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-3.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive/InjectAdapter.java new file mode 100644 index 00000000000..ef8996839e6 --- /dev/null +++ b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-3.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive/InjectAdapter.java @@ -0,0 +1,17 @@ +package datadog.trace.instrumentation.quarkus_rest_client_reactive; + +import datadog.context.propagation.CarrierSetter; +import jakarta.ws.rs.core.MultivaluedMap; +import javax.annotation.ParametersAreNonnullByDefault; + +@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-3.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive/QuarkusRestClientDecorator.java b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-3.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive/QuarkusRestClientDecorator.java new file mode 100644 index 00000000000..08436cc268c --- /dev/null +++ b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-3.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive/QuarkusRestClientDecorator.java @@ -0,0 +1,55 @@ +package datadog.trace.instrumentation.quarkus_rest_client_reactive; + +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import datadog.trace.bootstrap.instrumentation.decorator.HttpClientDecorator; +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientResponseContext; +import java.net.URI; + +public class QuarkusRestClientDecorator + extends HttpClientDecorator { + + public static final CharSequence QUARKUS_REST_CLIENT = + UTF8BytesString.create("quarkus-rest-client-reactive"); + public static final QuarkusRestClientDecorator DECORATE = new QuarkusRestClientDecorator(); + 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-3.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive/QuarkusRestClientInstrumentation.java b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-3.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive/QuarkusRestClientInstrumentation.java new file mode 100644 index 00000000000..a23a9a8a425 --- /dev/null +++ b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-3.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive/QuarkusRestClientInstrumentation.java @@ -0,0 +1,57 @@ +package datadog.trace.instrumentation.quarkus_rest_client_reactive; + +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 QuarkusRestClientInstrumentation extends InstrumenterModule.Tracing + implements Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice { + + public QuarkusRestClientInstrumentation() { + 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 + ".QuarkusRestClientDecorator", + packageName + ".QuarkusRestClientTracingFilter", + packageName + ".InjectAdapter", + }; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isMethod().and(named("build")).and(takesArgument(0, Class.class)), + QuarkusRestClientInstrumentation.class.getName() + "$RestClientBuilderAdvice"); + } + + public static class RestClientBuilderAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void registerFilter(@Advice.This final RestClientBuilder builder) { + builder.register(QuarkusRestClientTracingFilter.class); + } + } +} diff --git a/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-3.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive/QuarkusRestClientTracingFilter.java b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-3.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive/QuarkusRestClientTracingFilter.java new file mode 100644 index 00000000000..ab87d3cd38a --- /dev/null +++ b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-3.0/src/main/java/datadog/trace/instrumentation/quarkus_rest_client_reactive/QuarkusRestClientTracingFilter.java @@ -0,0 +1,47 @@ +package datadog.trace.instrumentation.quarkus_rest_client_reactive; + +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.InjectAdapter.SETTER; +import static datadog.trace.instrumentation.quarkus_rest_client_reactive.QuarkusRestClientDecorator.DECORATE; +import static datadog.trace.instrumentation.quarkus_rest_client_reactive.QuarkusRestClientDecorator.QUARKUS_REST_CLIENT; +import static datadog.trace.instrumentation.quarkus_rest_client_reactive.QuarkusRestClientDecorator.QUARKUS_REST_CLIENT_CALL; + +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import jakarta.annotation.Priority; +import jakarta.ws.rs.Priorities; +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.client.ClientResponseContext; +import jakarta.ws.rs.client.ClientResponseFilter; + +@Priority(Priorities.HEADER_DECORATOR) +public class QuarkusRestClientTracingFilter 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-3.0/src/test/java/datadog/trace/instrumentation/quarkus_rest_client_reactive/QuarkusRestClientInstrumentationTest.java b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-3.0/src/test/java/datadog/trace/instrumentation/quarkus_rest_client_reactive/QuarkusRestClientInstrumentationTest.java new file mode 100644 index 00000000000..d5c2731021e --- /dev/null +++ b/dd-java-agent/instrumentation/quarkus/quarkus-rest-client-reactive-3.0/src/test/java/datadog/trace/instrumentation/quarkus_rest_client_reactive/QuarkusRestClientInstrumentationTest.java @@ -0,0 +1,98 @@ +package datadog.trace.instrumentation.quarkus_rest_client_reactive; + +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 jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.concurrent.atomic.AtomicReference; +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 QuarkusRestClientInstrumentationTest 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"); + + // Verify a span is created — span type is "http" for HTTP client spans + 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..23196b5c805 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-3.0", ":dd-java-agent:instrumentation:quartz-2.0", ":dd-java-agent:instrumentation:rabbitmq-amqp-2.7", ":dd-java-agent:instrumentation:ratpack-1.5",