Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.+'
}
Original file line number Diff line number Diff line change
@@ -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<MultivaluedMap<String, Object>> {

public static final InjectAdapter SETTER = new InjectAdapter();

@Override
public void set(
final MultivaluedMap<String, Object> headers, final String key, final String value) {
headers.putSingle(key, value);
}
}
Original file line number Diff line number Diff line change
@@ -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<ClientRequestContext, ClientResponseContext> {

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);
}
}
Original file line number Diff line number Diff line change
@@ -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<TypeDescription> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String> capturedTraceId = new AtomicReference<>();
private static final AtomicReference<String> 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")));
}
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down