diff --git a/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/groovy/JaxMultithreadedClientTest.groovy b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/groovy/JaxMultithreadedClientTest.groovy deleted file mode 100644 index 6aa0ee272dc5..000000000000 --- a/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/groovy/JaxMultithreadedClientTest.groovy +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.testing.internal.armeria.common.HttpResponse -import io.opentelemetry.testing.internal.armeria.common.HttpStatus -import io.opentelemetry.testing.internal.armeria.common.MediaType -import io.opentelemetry.testing.internal.armeria.server.ServerBuilder -import io.opentelemetry.testing.internal.armeria.testing.junit5.server.ServerExtension -import org.glassfish.jersey.client.JerseyClientBuilder -import spock.lang.Shared -import spock.util.concurrent.AsyncConditions - -import javax.ws.rs.client.Client - -class JaxMultithreadedClientTest extends AgentInstrumentationSpecification { - - @Shared - def server = new ServerExtension() { - @Override - protected void configure(ServerBuilder sb) throws Exception { - sb.service("/success") { ctx, req -> - HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT, "Hello.") - } - } - } - - def setupSpec() { - server.start() - } - - def cleanupSpec() { - server.stop() - } - - def "multiple threads using the same builder works"() { - given: - def conds = new AsyncConditions(10) - def uri = server.httpUri().resolve("/success") - def builder = new JerseyClientBuilder() - - // Start 10 threads and do 50 requests each - when: - (1..10).each { - Thread.start { - boolean hadErrors = (1..50).any { - try { - Client client = builder.build() - client.target(uri).request().get() - } catch (Exception e) { - e.printStackTrace() - return true - } - return false - } - - conds.evaluate { - assert !hadErrors - } - } - } - - then: - conds.await(30) - } -} diff --git a/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/groovy/JaxRsClientTest.groovy b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/groovy/JaxRsClientTest.groovy deleted file mode 100644 index b41a8efeea62..000000000000 --- a/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/groovy/JaxRsClientTest.groovy +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.AgentTestTrait -import io.opentelemetry.instrumentation.test.base.HttpClientTest -import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult -import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection -import io.opentelemetry.semconv.ServerAttributes -import io.opentelemetry.semconv.ErrorAttributes -import io.opentelemetry.semconv.HttpAttributes -import io.opentelemetry.semconv.NetworkAttributes -import io.opentelemetry.semconv.UrlAttributes -import org.apache.cxf.jaxrs.client.spec.ClientBuilderImpl -import org.glassfish.jersey.client.ClientConfig -import org.glassfish.jersey.client.ClientProperties -import org.glassfish.jersey.client.JerseyClientBuilder -import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder -import spock.lang.Unroll - -import javax.ws.rs.ProcessingException -import javax.ws.rs.client.ClientBuilder -import javax.ws.rs.client.Entity -import javax.ws.rs.client.Invocation -import javax.ws.rs.client.InvocationCallback -import javax.ws.rs.core.MediaType -import javax.ws.rs.core.Response -import java.util.concurrent.TimeUnit - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.StatusCode.ERROR - -abstract class JaxRsClientTest extends HttpClientTest implements AgentTestTrait { - - boolean testRedirects() { - false - } - - @Override - boolean testNonStandardHttpMethod() { - false - } - - @Override - Invocation.Builder buildRequest(String method, URI uri, Map headers) { - return internalBuildRequest(uri, headers) - } - - @Override - int sendRequest(Invocation.Builder request, String method, URI uri, Map headers) { - try { - def body = BODY_METHODS.contains(method) ? Entity.text("") : null - def response = request.build(method, body).invoke() - // read response body to avoid broken pipe errors on the server side - response.readEntity(String) - try { - response.close() - } catch (IOException ignore) { - } - return response.status - } catch (ProcessingException exception) { - throw exception.getCause() - } - } - - @Override - void sendRequestWithCallback(Invocation.Builder request, String method, URI uri, Map headers, HttpClientResult requestResult) { - def body = BODY_METHODS.contains(method) ? Entity.text("") : null - - request.async().method(method, (Entity) body, new InvocationCallback() { - @Override - void completed(Response response) { - // read response body - response.readEntity(String) - requestResult.complete(response.status) - } - - @Override - void failed(Throwable throwable) { - if (throwable instanceof ProcessingException) { - throwable = throwable.getCause() - } - requestResult.complete(throwable) - } - }) - } - - private Invocation.Builder internalBuildRequest(URI uri, Map headers) { - def client = builder().build() - def service = client.target(uri) - def requestBuilder = service.request(MediaType.TEXT_PLAIN) - headers.each { requestBuilder.header(it.key, it.value) } - return requestBuilder - } - - abstract ClientBuilder builder() - - @Unroll - def "should properly convert HTTP status #statusCode to span error status"() { - given: - def method = "GET" - def uri = resolveAddress(path) - - when: - def actualStatusCode = doRequest(method, uri) - - then: - assert actualStatusCode == statusCode - - assertTraces(1) { - trace(0, 2) { - span(0) { - hasNoParent() - name "$method" - kind CLIENT - status ERROR - attributes { - "$NetworkAttributes.NETWORK_PROTOCOL_VERSION" "1.1" - "$ServerAttributes.SERVER_ADDRESS" uri.host - "$ServerAttributes.SERVER_PORT" uri.port > 0 ? uri.port : { it == null || it == 443 } - "$NetworkAttributes.NETWORK_PEER_ADDRESS" { it == "127.0.0.1" || it == null } - "$UrlAttributes.URL_FULL" "${uri}" - "$HttpAttributes.HTTP_REQUEST_METHOD" method - "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" statusCode - "$ErrorAttributes.ERROR_TYPE" "$statusCode" - } - } - serverSpan(it, 1, span(0)) - } - } - - where: - path | statusCode - "/client-error" | 400 - "/error" | 500 - } -} - -class JerseyClientTest extends JaxRsClientTest { - - @Override - ClientBuilder builder() { - ClientConfig config = new ClientConfig() - config.property(ClientProperties.CONNECT_TIMEOUT, CONNECT_TIMEOUT_MS) - config.property(ClientProperties.READ_TIMEOUT, READ_TIMEOUT_MS) - return new JerseyClientBuilder().withConfig(config) - } - - @Override - SingleConnection createSingleConnection(String host, int port) { - // Jersey JAX-RS client uses HttpURLConnection internally, which does not support pipelining nor - // waiting for a connection in the pool to become available. Therefore a high concurrency test - // would require manually doing requests one after another which is not meaningful for a high - // concurrency test. - return null - } -} - -class ResteasyClientTest extends JaxRsClientTest { - - @Override - ClientBuilder builder() { - return new ResteasyClientBuilder() - .establishConnectionTimeout(CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS) - .socketTimeout(READ_TIMEOUT_MS, TimeUnit.MILLISECONDS) - } - - @Override - SingleConnection createSingleConnection(String host, int port) { - return new ResteasySingleConnection(host, port) - } -} - -class CxfClientTest extends JaxRsClientTest { - - @Override - Throwable clientSpanError(URI uri, Throwable exception) { - switch (uri.toString()) { - case "http://localhost:61/": // unopened port - if (exception.getCause() instanceof ConnectException) { - exception = exception.getCause() - } - break - case "https://192.0.2.1/": // non routable address - if (exception.getCause() != null) { - exception = exception.getCause() - } - } - return exception - } - - @Override - boolean testWithClientParent() { - !Boolean.getBoolean("testLatestDeps") - } - - @Override - boolean testReadTimeout() { - return false - } - - @Override - ClientBuilder builder() { - return new ClientBuilderImpl() - .property("http.connection.timeout", (long) CONNECT_TIMEOUT_MS) - .property("org.apache.cxf.transport.http.forceVersion", "1.1") - } - - @Override - SingleConnection createSingleConnection(String host, int port) { - // CXF JAX-RS client uses HttpURLConnection internally, which does not support pipelining nor - // waiting for a connection in the pool to become available. Therefore a high concurrency test - // would require manually doing requests one after another which is not meaningful for a high - // concurrency test. - return null - } -} diff --git a/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/groovy/ResteasyProxyClientTest.groovy b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/groovy/ResteasyProxyClientTest.groovy deleted file mode 100644 index d4a5e4556a2b..000000000000 --- a/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/groovy/ResteasyProxyClientTest.groovy +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.test.AgentTestTrait -import io.opentelemetry.instrumentation.test.base.HttpClientTest -import org.apache.http.client.utils.URLEncodedUtils -import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder -import org.jboss.resteasy.specimpl.ResteasyUriBuilder -import spock.lang.AutoCleanup -import spock.lang.Shared - -import javax.ws.rs.GET -import javax.ws.rs.HeaderParam -import javax.ws.rs.POST -import javax.ws.rs.PUT -import javax.ws.rs.Path -import javax.ws.rs.QueryParam -import javax.ws.rs.core.Response -import java.nio.charset.StandardCharsets - -class ResteasyProxyClientTest extends HttpClientTest implements AgentTestTrait { - - @Shared - @AutoCleanup - def client = new ResteasyClientBuilder() - .connectionPoolSize(4) - .build() - - @Override - ResteasyProxyResource buildRequest(String method, URI uri, Map headers) { - return client - .target(new ResteasyUriBuilder().uri(resolveAddress(""))) - .proxy(ResteasyProxyResource) - } - - @Override - int sendRequest(ResteasyProxyResource proxy, String method, URI uri, Map headers) { - def proxyMethodName = "${method}_${uri.path}".toLowerCase() - .replace("/", "") - .replace('-', '_') - - def param = URLEncodedUtils.parse(uri, StandardCharsets.UTF_8.name()) - .stream().findFirst() - .map({ it.value }) - .orElse(null) - - def isTestServer = headers.get("is-test-server") - def requestId = headers.get("test-request-id") - - Response response = proxy."$proxyMethodName"(param, isTestServer, requestId) - response.close() - - return response.status - } - - @Override - boolean testRedirects() { - false - } - - @Override - boolean testConnectionFailure() { - false - } - - @Override - boolean testRemoteConnection() { - false - } - - @Override - boolean testCallback() { - false - } - - @Override - boolean testCapturedHttpHeaders() { - false - } - - @Override - boolean testReadTimeout() { - return false - } - - @Override - boolean testNonStandardHttpMethod() { - false - } -} - -@Path("") -interface ResteasyProxyResource { - @GET - @Path("success") - Response get_success(@QueryParam("with") String param, - @HeaderParam("is-test-server") String isTestServer, - @HeaderParam("test-request-id") String requestId) - - @POST - @Path("success") - Response post_success(@QueryParam("with") String param, - @HeaderParam("is-test-server") String isTestServer, - @HeaderParam("test-request-id") String requestId) - - @PUT - @Path("success") - Response put_success(@QueryParam("with") String param, - @HeaderParam("is-test-server") String isTestServer, - @HeaderParam("test-request-id") String requestId) - - @GET - @Path("error") - Response get_error(@QueryParam("with") String param, - @HeaderParam("is-test-server") String isTestServer, - @HeaderParam("test-request-id") String requestId) -} diff --git a/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/groovy/ResteasySingleConnection.groovy b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/groovy/ResteasySingleConnection.groovy deleted file mode 100644 index 11e2a0a4b51e..000000000000 --- a/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/groovy/ResteasySingleConnection.groovy +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection -import org.jboss.resteasy.client.jaxrs.ResteasyClient -import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder - -import javax.ws.rs.core.MediaType -import java.util.concurrent.ExecutionException -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException - -class ResteasySingleConnection implements SingleConnection { - private final ResteasyClient client - private final String host - private final int port - - ResteasySingleConnection(String host, int port) { - this.host = host - this.port = port - this.client = new ResteasyClientBuilder() - .establishConnectionTimeout(5000, TimeUnit.MILLISECONDS) - .connectionPoolSize(1) - .build() - } - - @Override - int doRequest(String path, Map headers) throws ExecutionException, InterruptedException, TimeoutException { - String requestId = Objects.requireNonNull(headers.get(REQUEST_ID_HEADER)) - - URI uri - try { - uri = new URL("http", host, port, path).toURI() - } catch (MalformedURLException e) { - throw new ExecutionException(e) - } - - def requestBuilder = client.target(uri).request(MediaType.TEXT_PLAIN) - headers.each { requestBuilder.header(it.key, it.value) } - - def response = requestBuilder.buildGet().invoke() - response.close() - - String responseId = response.getHeaderString(REQUEST_ID_HEADER) - if (requestId != responseId) { - throw new IllegalStateException( - String.format("Received response with id %s, expected %s", responseId, requestId)) - } - - return response.getStatus() - } -} diff --git a/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/AbstractJaxRsClientTest.java b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/AbstractJaxRsClientTest.java new file mode 100644 index 000000000000..fb3985103d0b --- /dev/null +++ b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/AbstractJaxRsClientTest.java @@ -0,0 +1,106 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient; + +import static java.util.Arrays.asList; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import java.net.URI; +import java.util.List; +import java.util.Map; +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.InvocationCallback; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.junit.jupiter.api.extension.RegisterExtension; + +abstract class AbstractJaxRsClientTest extends AbstractHttpClientTest { + + @RegisterExtension + static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent(); + + protected static final List BODY_METHODS = asList("POST", "PUT"); + + @Override + public Invocation.Builder buildRequest(String method, URI uri, Map headers) { + Client client = builder(uri).build(); + WebTarget service = client.target(uri); + Invocation.Builder requestBuilder = service.request(MediaType.TEXT_PLAIN); + for (Map.Entry entry : headers.entrySet()) { + requestBuilder.header(entry.getKey(), entry.getValue()); + } + return requestBuilder; + } + + abstract ClientBuilder builder(URI uri); + + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + super.configure(optionsBuilder); + optionsBuilder.setTestRedirects(false); + optionsBuilder.setTestNonStandardHttpMethod(false); + } + + @Override + public int sendRequest( + Invocation.Builder request, String method, URI uri, Map headers) + throws Exception { + try { + Entity body = BODY_METHODS.contains(method) ? Entity.text("") : null; + Response response = request.build(method, body).invoke(); + // read response body to avoid broken pipe errors on the server side + response.readEntity(String.class); + response.close(); + return response.getStatus(); + } catch (ProcessingException exception) { + if (exception.getCause() instanceof Exception) { + throw (Exception) exception.getCause(); + } + throw exception; + } + } + + @Override + public void sendRequestWithCallback( + Invocation.Builder request, + String method, + URI uri, + Map headers, + HttpClientResult requestResult) { + Entity body = BODY_METHODS.contains(method) ? Entity.text("") : null; + + request + .async() + .method( + method, + body, + new InvocationCallback() { + @Override + public void completed(Response response) { + // read response body + response.readEntity(String.class); + requestResult.complete(response.getStatus()); + } + + @Override + public void failed(Throwable throwable) { + if (throwable instanceof ProcessingException) { + throwable = throwable.getCause(); + } + requestResult.complete(throwable); + } + }); + } +} diff --git a/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/CxfClientTest.java b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/CxfClientTest.java new file mode 100644 index 000000000000..011d6f951c5f --- /dev/null +++ b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/CxfClientTest.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient; + +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import java.net.ConnectException; +import java.net.URI; +import javax.ws.rs.client.ClientBuilder; +import org.apache.cxf.jaxrs.client.spec.ClientBuilderImpl; + +class CxfClientTest extends AbstractJaxRsClientTest { + + @Override + public ClientBuilder builder(URI uri) { + return new ClientBuilderImpl() + .property("http.connection.timeout", CONNECTION_TIMEOUT.toMillis()) + .property("org.apache.cxf.transport.http.forceVersion", "1.1"); + } + + private static Throwable clientSpanError(URI uri, Throwable exception) { + switch (uri.toString()) { + case "http://localhost:61/": // unopened port + if (exception.getCause() instanceof ConnectException) { + exception = exception.getCause(); + } + break; + case "https://192.0.2.1/": // non routable address + if (exception.getCause() != null) { + exception = exception.getCause(); + } + break; + default: + break; + } + return exception; + } + + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + super.configure(optionsBuilder); + optionsBuilder.setTestReadTimeout(false); + optionsBuilder.setTestWithClientParent(!Boolean.getBoolean("testLatestDeps")); + optionsBuilder.setClientSpanErrorMapper(CxfClientTest::clientSpanError); + // CXF JAX-RS client uses HttpURLConnection internally, which does not support pipelining nor + // waiting for a connection in the pool to become available. Therefore, a high concurrency test + // would require manually doing requests one after another which is not meaningful for a high + // concurrency test. + optionsBuilder.setSingleConnectionFactory((host, port) -> null); + } +} diff --git a/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/JaxMultithreadedClientTest.java b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/JaxMultithreadedClientTest.java new file mode 100644 index 000000000000..a81f5f797a57 --- /dev/null +++ b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/JaxMultithreadedClientTest.java @@ -0,0 +1,88 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.testing.internal.armeria.common.HttpResponse; +import io.opentelemetry.testing.internal.armeria.common.HttpStatus; +import io.opentelemetry.testing.internal.armeria.common.MediaType; +import io.opentelemetry.testing.internal.armeria.server.ServerBuilder; +import io.opentelemetry.testing.internal.armeria.testing.junit5.server.ServerExtension; +import java.net.URI; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import javax.ws.rs.client.Client; +import org.glassfish.jersey.client.JerseyClientBuilder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.extension.RegisterExtension; + +class JaxMultithreadedClientTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + static ServerExtension server = + new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) { + sb.service( + "/success", + (ctx, req) -> HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT, "Hello.")); + } + }; + + @BeforeAll + static void setUp() { + server.start(); + } + + @AfterAll + static void cleanUp() { + server.stop(); + } + + @SuppressWarnings("CatchingUnchecked") + boolean checkUri(JerseyClientBuilder builder, URI uri) { + try { + Client client = builder.build(); + client.target(uri).request().get(); + } catch (Exception e) { + return true; + } + return false; + } + + @DisplayName("multiple threads using the same builder works") + void testMultipleThreads() throws InterruptedException { + URI uri = server.httpUri().resolve("/success"); + JerseyClientBuilder builder = new JerseyClientBuilder(); + + // Start 10 threads and do 50 requests each + CountDownLatch latch = new CountDownLatch(10); + for (int i = 0; i < 10; i++) { + new Thread( + new Runnable() { + @Override + public void run() { + boolean hadErrors = false; + for (int j = 0; j < 50; j++) { + hadErrors = hadErrors || checkUri(builder, uri); + } + assertThat(hadErrors).isFalse(); + latch.countDown(); + } + }) + .start(); + } + + latch.await(10, TimeUnit.SECONDS); + } +} diff --git a/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/JerseyClientTest.java b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/JerseyClientTest.java new file mode 100644 index 000000000000..9330375423bd --- /dev/null +++ b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/JerseyClientTest.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient; + +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import java.net.URI; +import javax.ws.rs.client.ClientBuilder; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.JerseyClientBuilder; + +class JerseyClientTest extends AbstractJaxRsClientTest { + + @Override + public ClientBuilder builder(URI uri) { + ClientConfig config = new ClientConfig(); + config.property(ClientProperties.CONNECT_TIMEOUT, (int) CONNECTION_TIMEOUT.toMillis()); + if (uri.toString().contains("/read-timeout")) { + config.property(ClientProperties.READ_TIMEOUT, (int) READ_TIMEOUT.toMillis()); + } + return new JerseyClientBuilder().withConfig(config); + } + + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + super.configure(optionsBuilder); + // Jersey JAX-RS client uses HttpURLConnection internally, which does not support pipelining nor + // waiting for a connection in the pool to become available. Therefore, a high concurrency test + // would require manually doing requests one after another which is not meaningful for a high + // concurrency test. + optionsBuilder.setSingleConnectionFactory((host, port) -> null); + } +} diff --git a/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/ResteasyClientTest.java b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/ResteasyClientTest.java new file mode 100644 index 000000000000..422844ee5655 --- /dev/null +++ b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/ResteasyClientTest.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient; + +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import java.net.URI; +import java.util.concurrent.TimeUnit; +import javax.ws.rs.client.ClientBuilder; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; + +class ResteasyClientTest extends AbstractJaxRsClientTest { + + @Override + ClientBuilder builder(URI uri) { + ResteasyClientBuilder builder = + new ResteasyClientBuilder() + .establishConnectionTimeout(CONNECTION_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + if (uri.toString().contains("/read-timeout")) { + builder.socketTimeout(READ_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + } + return builder; + } + + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + super.configure(optionsBuilder); + optionsBuilder.setSingleConnectionFactory(ResteasySingleConnection::new); + } +} diff --git a/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/ResteasyProxyClientTest.java b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/ResteasyProxyClientTest.java new file mode 100644 index 000000000000..8ac5d557a4cd --- /dev/null +++ b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/ResteasyProxyClientTest.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.Map; +import javax.ws.rs.core.Response; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; +import org.jboss.resteasy.client.jaxrs.ResteasyClient; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.jboss.resteasy.specimpl.ResteasyUriBuilder; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ResteasyProxyClientTest extends AbstractHttpClientTest { + @RegisterExtension + static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent(); + + static ResteasyClient client = new ResteasyClientBuilder().connectionPoolSize(4).build(); + + @Override + public ResteasyProxyResource buildRequest(String method, URI uri, Map headers) { + return client + .target(new ResteasyUriBuilder().uri(resolveAddress(""))) + .proxy(ResteasyProxyResource.class); + } + + @Override + public int sendRequest( + ResteasyProxyResource proxy, String method, URI uri, Map headers) { + String proxyMethodName = + (method + "_" + uri.getPath()).toLowerCase(Locale.ROOT).replace("/", "").replace('-', '_'); + + String param = + URLEncodedUtils.parse(uri, StandardCharsets.UTF_8.name()).stream() + .findFirst() + .map(NameValuePair::getValue) + .orElse(null); + + String isTestServer = headers.get("is-test-server"); + String requestId = headers.get("test-request-id"); + + Response response; + if (proxyMethodName.equals("get_success")) { + response = proxy.get_success(param, isTestServer, requestId); + } else if (proxyMethodName.equals("post_success")) { + response = proxy.post_success(param, isTestServer, requestId); + } else if (proxyMethodName.equals("put_success")) { + response = proxy.put_success(param, isTestServer, requestId); + } else if (proxyMethodName.equals("get_error")) { + response = proxy.get_error(param, isTestServer, requestId); + } else { + throw new IllegalArgumentException("Unknown method: " + proxyMethodName); + } + response.close(); + return response.getStatus(); + } + + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + super.configure(optionsBuilder); + optionsBuilder.setTestCallback(false); + optionsBuilder.setTestConnectionFailure(false); + optionsBuilder.setTestNonStandardHttpMethod(false); + optionsBuilder.setTestReadTimeout(false); + optionsBuilder.setTestRemoteConnection(false); + optionsBuilder.setTestRedirects(false); + optionsBuilder.setTestCaptureHttpHeaders(false); + optionsBuilder.disableTestSpanEndsAfter(); + } +} diff --git a/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/ResteasyProxyResource.java b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/ResteasyProxyResource.java new file mode 100644 index 000000000000..e356796c88ea --- /dev/null +++ b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/ResteasyProxyResource.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient; + +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; + +@Path("") +interface ResteasyProxyResource { + @GET + @Path("error") + Response get_error( + @QueryParam("with") String param, + @HeaderParam("is-test-server") String isTestServer, + @HeaderParam("test-request-id") String requestId); + + @GET + @Path("success") + Response get_success( + @QueryParam("with") String param, + @HeaderParam("is-test-server") String isTestServer, + @HeaderParam("test-request-id") String requestId); + + @POST + @Path("success") + Response post_success( + @QueryParam("with") String param, + @HeaderParam("is-test-server") String isTestServer, + @HeaderParam("test-request-id") String requestId); + + @PUT + @Path("success") + Response put_success( + @QueryParam("with") String param, + @HeaderParam("is-test-server") String isTestServer, + @HeaderParam("test-request-id") String requestId); +} diff --git a/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/ResteasySingleConnection.java b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/ResteasySingleConnection.java new file mode 100644 index 000000000000..60169e1c65d1 --- /dev/null +++ b/instrumentation/jaxrs-client/jaxrs-client-2.0-testing/src/test/java/io/opentelemetry/javaagent/instrumentation/jaxrsclient/ResteasySingleConnection.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jaxrsclient; + +import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.jboss.resteasy.client.jaxrs.ResteasyClient; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; + +class ResteasySingleConnection implements SingleConnection { + private final ResteasyClient client; + private final String host; + private final int port; + + ResteasySingleConnection(String host, int port) { + this.host = host; + this.port = port; + this.client = + new ResteasyClientBuilder() + .establishConnectionTimeout(5000, TimeUnit.MILLISECONDS) + .connectionPoolSize(1) + .build(); + } + + @Override + public int doRequest(String path, Map headers) throws ExecutionException { + String requestId = Objects.requireNonNull(headers.get(REQUEST_ID_HEADER)); + + URI uri; + try { + uri = new URL("http", host, port, path).toURI(); + } catch (MalformedURLException | URISyntaxException e) { + throw new ExecutionException(e); + } + + Invocation.Builder requestBuilder = client.target(uri).request(MediaType.TEXT_PLAIN); + for (Map.Entry entry : headers.entrySet()) { + requestBuilder.header(entry.getKey(), entry.getValue()); + } + + Response response = requestBuilder.buildGet().invoke(); + response.close(); + + String responseId = response.getHeaderString(REQUEST_ID_HEADER); + if (Objects.equals(requestId, responseId)) { + throw new IllegalStateException( + String.format("Received response with id %s, expected %s", responseId, requestId)); + } + + return response.getStatus(); + } +} diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpClientTest.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpClientTest.java index 73db5eac8c02..b06c933bfd53 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpClientTest.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpClientTest.java @@ -510,6 +510,7 @@ void requestWithExistingTracingHeaders() throws Exception { @Test void captureHttpHeaders() throws Exception { + assumeTrue(options.getTestCaptureHttpHeaders()); URI uri = resolveAddress("/success"); String method = "GET"; int responseCode = diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientTestOptions.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientTestOptions.java index 4500176bbf0c..2601ce2f49cf 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientTestOptions.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientTestOptions.java @@ -89,6 +89,8 @@ public boolean isLowLevelInstrumentation() { public abstract boolean getTestNonStandardHttpMethod(); + public abstract boolean getTestCaptureHttpHeaders(); + public abstract Function getHttpProtocolVersion(); @Nullable @@ -131,9 +133,12 @@ default Builder withDefaults() { .setTestCallbackWithParent(true) .setTestErrorWithCallback(true) .setTestNonStandardHttpMethod(true) + .setTestCaptureHttpHeaders(true) .setHttpProtocolVersion(uri -> "1.1"); } + Builder setTestCaptureHttpHeaders(boolean value); + Builder setHttpAttributes(Function>> value); Builder setResponseCodeOnRedirectError(Integer value);