From 47be4a16b4c1ab7d744e892de5ee0c1e9e402084 Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Tue, 27 Jul 2021 21:55:40 +0900 Subject: [PATCH] Convert HttpClientTest to JUnit (#3652) * Migrate HttpClientTest to junit to allow both Java or spock tests. * More * Update * Finish * Cleanup * Better stack * Java 15 * Merge * Fix name * Cleanup * ? extends * Moar --- .../AkkaHttpClientInstrumentationTest.groovy | 5 +- .../groovy/ApacheHttpAsyncClientTest.groovy | 3 +- .../test/groovy/ApacheHttpClientTest.groovy | 3 +- .../test/groovy/ApacheHttpClientTest.groovy | 3 +- .../armeria/v1_3/ArmeriaHttpClientTest.groovy | 17 - .../armeria/v1_3/ArmeriaHttpClientTest.java | 23 + .../armeria/v1_3/ArmeriaHttpClientTest.groovy | 34 - .../armeria/v1_3/ArmeriaHttpClientTest.java | 41 + .../v1_3/AbstractArmeriaHttpClientTest.groovy | 77 -- .../v1_3/AbstractArmeriaHttpClientTest.java | 96 ++ .../test/groovy/AsyncHttpClientTest.groovy | 5 +- .../test/groovy/AsyncHttpClientTest.groovy | 3 +- .../test/groovy/SpringRestTemplateTest.groovy | 3 +- .../src/test/groovy/JdkHttpClientTest.groovy | 3 +- .../src/test/groovy/JaxRsClientTest.groovy | 5 +- .../groovy/ResteasySingleConnection.groovy | 2 +- .../v9_2/AbstractJettyClient9Test.groovy | 15 +- .../src/test/groovy/Netty38ClientTest.groovy | 8 +- .../src/test/groovy/Netty40ClientTest.groovy | 3 +- .../src/test/groovy/Netty41ClientTest.groovy | 5 +- .../test/groovy/SingleNettyConnection.java | 2 +- .../src/test/groovy/OkHttp2Test.groovy | 3 +- .../okhttp/v3_0/AbstractOkHttp3Test.groovy | 3 +- .../main/groovy/PlayWsClientTestBase.groovy | 10 +- .../groovy/client/PlayWsClientTest.groovy | 5 +- .../client/RatpackHttpClientTest.groovy | 17 +- .../client/RatpackPooledHttpClientTest.groovy | 2 +- .../AbstractReactorNettyHttpClientTest.groovy | 8 +- .../v0_9/ReactorNettyHttpClientTest.groovy | 2 +- .../AbstractReactorNettyHttpClientTest.groovy | 8 +- .../v1_0/ReactorNettyHttpClientTest.groovy | 2 +- .../RestTemplateInstrumentationTest.groovy | 3 +- .../SpringWebFluxSingleConnection.groovy | 2 +- .../client/SpringWebfluxHttpClientTest.groovy | 10 +- .../groovy/client/VertxHttpClientTest.groovy | 5 +- .../groovy/client/VertxSingleConnection.java | 2 +- .../VertxRxCircuitBreakerWebClientTest.groovy | 5 +- .../groovy/client/VertxRxWebClientTest.groovy | 10 +- .../java/client/VertxRxSingleConnection.java | 2 +- testing-common/build.gradle.kts | 7 +- .../test/base/HttpClientTest.groovy | 1100 ++++----------- .../test/utils/TraceUtils.groovy | 9 - .../testing/InstrumentationTestRunner.java | 47 + .../junit/InstrumentationExtension.java | 43 +- .../junit/http/AbstractHttpClientTest.java | 1218 +++++++++++++++++ .../HttpClientInstrumentationExtension.java | 69 + .../junit/http/HttpClientTestServer.java | 122 ++ .../testing/junit/http}/SingleConnection.java | 2 +- 48 files changed, 1962 insertions(+), 1110 deletions(-) delete mode 100644 instrumentation/armeria-1.3/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.groovy create mode 100644 instrumentation/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.java delete mode 100644 instrumentation/armeria-1.3/library/src/test/groovy/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.groovy create mode 100644 instrumentation/armeria-1.3/library/src/test/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.java delete mode 100644 instrumentation/armeria-1.3/testing/src/main/groovy/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpClientTest.groovy create mode 100644 instrumentation/armeria-1.3/testing/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpClientTest.java create mode 100644 testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpClientTest.java create mode 100644 testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientInstrumentationExtension.java create mode 100644 testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientTestServer.java rename testing-common/src/main/{groovy/io/opentelemetry/instrumentation/test/base => java/io/opentelemetry/instrumentation/testing/junit/http}/SingleConnection.java (93%) diff --git a/instrumentation/akka-http-10.0/javaagent/src/test/groovy/AkkaHttpClientInstrumentationTest.groovy b/instrumentation/akka-http-10.0/javaagent/src/test/groovy/AkkaHttpClientInstrumentationTest.groovy index b21daaac42d0..e673a5a98cb8 100644 --- a/instrumentation/akka-http-10.0/javaagent/src/test/groovy/AkkaHttpClientInstrumentationTest.groovy +++ b/instrumentation/akka-http-10.0/javaagent/src/test/groovy/AkkaHttpClientInstrumentationTest.groovy @@ -15,7 +15,8 @@ import akka.http.javadsl.model.headers.RawHeader import akka.stream.ActorMaterializer import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpClientTest -import io.opentelemetry.instrumentation.test.base.SingleConnection +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection import java.util.concurrent.TimeUnit import spock.lang.Shared @@ -46,7 +47,7 @@ class AkkaHttpClientInstrumentationTest extends HttpClientTest impl } @Override - void sendRequestWithCallback(HttpRequest request, String method, URI uri, Map headers, RequestResult requestResult) { + void sendRequestWithCallback(HttpRequest request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { Http.get(system).singleRequest(request, materializer).whenComplete {response, throwable -> if (throwable == null) { response.discardEntityBytes(materializer) diff --git a/instrumentation/apache-httpasyncclient-4.1/javaagent/src/test/groovy/ApacheHttpAsyncClientTest.groovy b/instrumentation/apache-httpasyncclient-4.1/javaagent/src/test/groovy/ApacheHttpAsyncClientTest.groovy index 32b1f7b4e9dc..2e082c9ee9be 100644 --- a/instrumentation/apache-httpasyncclient-4.1/javaagent/src/test/groovy/ApacheHttpAsyncClientTest.groovy +++ b/instrumentation/apache-httpasyncclient-4.1/javaagent/src/test/groovy/ApacheHttpAsyncClientTest.groovy @@ -6,6 +6,7 @@ import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest import io.opentelemetry.semconv.trace.attributes.SemanticAttributes import java.util.concurrent.CancellationException import org.apache.http.HttpResponse @@ -46,7 +47,7 @@ class ApacheHttpAsyncClientTest extends HttpClientTest implement } @Override - void sendRequestWithCallback(HttpUriRequest request, String method, URI uri, Map headers, RequestResult requestResult) { + void sendRequestWithCallback(HttpUriRequest request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { client.execute(request, new FutureCallback() { @Override void completed(HttpResponse httpResponse) { diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/test/groovy/ApacheHttpClientTest.groovy b/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/test/groovy/ApacheHttpClientTest.groovy index 334d5a2398d4..3cbe1414631a 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/test/groovy/ApacheHttpClientTest.groovy +++ b/instrumentation/apache-httpclient/apache-httpclient-4.0/javaagent/src/test/groovy/ApacheHttpClientTest.groovy @@ -6,6 +6,7 @@ import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest import io.opentelemetry.semconv.trace.attributes.SemanticAttributes import java.util.function.Consumer import org.apache.http.HttpHost @@ -59,7 +60,7 @@ abstract class ApacheHttpClientTest extends HttpClientTes } // compilation fails with @Override annotation on this method (groovy quirk?) - void sendRequestWithCallback(T request, String method, URI uri, Map headers, RequestResult requestResult) { + void sendRequestWithCallback(T request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { try { executeRequestWithCallback(request, uri) { it.entity?.content?.close() // Make sure the connection is closed. diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/groovy/ApacheHttpClientTest.groovy b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/groovy/ApacheHttpClientTest.groovy index 487f125587bb..50aebea26437 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/groovy/ApacheHttpClientTest.groovy +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/groovy/ApacheHttpClientTest.groovy @@ -6,6 +6,7 @@ import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest import io.opentelemetry.semconv.trace.attributes.SemanticAttributes import java.util.concurrent.TimeUnit import java.util.function.Consumer @@ -64,7 +65,7 @@ abstract class ApacheHttpClientTest extends HttpClientTes } // compilation fails with @Override annotation on this method (groovy quirk?) - void sendRequestWithCallback(T request, String method, URI uri, Map headers, RequestResult requestResult) { + void sendRequestWithCallback(T request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { try { executeRequestWithCallback(request, uri) { it.close() // Make sure the connection is closed. diff --git a/instrumentation/armeria-1.3/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.groovy b/instrumentation/armeria-1.3/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.groovy deleted file mode 100644 index 16bc9afa29d4..000000000000 --- a/instrumentation/armeria-1.3/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.groovy +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.armeria.v1_3 - -import com.linecorp.armeria.client.WebClientBuilder -import io.opentelemetry.instrumentation.armeria.v1_3.AbstractArmeriaHttpClientTest -import io.opentelemetry.instrumentation.test.AgentTestTrait - -class ArmeriaHttpClientTest extends AbstractArmeriaHttpClientTest implements AgentTestTrait { - @Override - WebClientBuilder configureClient(WebClientBuilder clientBuilder) { - return clientBuilder - } -} diff --git a/instrumentation/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.java b/instrumentation/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.java new file mode 100644 index 000000000000..83fa2e2da44d --- /dev/null +++ b/instrumentation/armeria-1.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.armeria.v1_3; + +import com.linecorp.armeria.client.WebClientBuilder; +import io.opentelemetry.instrumentation.armeria.v1_3.AbstractArmeriaHttpClientTest; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ArmeriaHttpClientTest extends AbstractArmeriaHttpClientTest { + + @RegisterExtension + static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent(); + + @Override + protected WebClientBuilder configureClient(WebClientBuilder clientBuilder) { + return clientBuilder; + } +} diff --git a/instrumentation/armeria-1.3/library/src/test/groovy/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.groovy b/instrumentation/armeria-1.3/library/src/test/groovy/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.groovy deleted file mode 100644 index 6bee41bddd3f..000000000000 --- a/instrumentation/armeria-1.3/library/src/test/groovy/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.groovy +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.armeria.v1_3 - -import com.linecorp.armeria.client.WebClientBuilder -import io.opentelemetry.instrumentation.test.LibraryTestTrait - -class ArmeriaHttpClientTest extends AbstractArmeriaHttpClientTest implements LibraryTestTrait { - @Override - WebClientBuilder configureClient(WebClientBuilder clientBuilder) { - return clientBuilder.decorator(ArmeriaTracing.create(getOpenTelemetry()).newClientDecorator()) - } - - // library instrumentation doesn't have a good way of suppressing nested CLIENT spans yet - @Override - boolean testWithClientParent() { - false - } - - // Agent users have automatic propagation through executor instrumentation, but library users - // should do manually using Armeria patterns. - @Override - boolean testCallbackWithParent() { - false - } - - @Override - boolean testErrorWithCallback() { - return false - } -} diff --git a/instrumentation/armeria-1.3/library/src/test/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.java b/instrumentation/armeria-1.3/library/src/test/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.java new file mode 100644 index 000000000000..30cd13ef8f52 --- /dev/null +++ b/instrumentation/armeria-1.3/library/src/test/java/io/opentelemetry/instrumentation/armeria/v1_3/ArmeriaHttpClientTest.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.armeria.v1_3; + +import com.linecorp.armeria.client.WebClientBuilder; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ArmeriaHttpClientTest extends AbstractArmeriaHttpClientTest { + + @RegisterExtension + static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forLibrary(); + + @Override + protected WebClientBuilder configureClient(WebClientBuilder clientBuilder) { + return clientBuilder.decorator( + ArmeriaTracing.create(testing.getOpenTelemetry()).newClientDecorator()); + } + + // library instrumentation doesn't have a good way of suppressing nested CLIENT spans yet + @Override + protected boolean testWithClientParent() { + return false; + } + + // Agent users have automatic propagation through executor instrumentation, but library users + // should do manually using Armeria patterns. + @Override + protected boolean testCallbackWithParent() { + return false; + } + + @Override + protected boolean testErrorWithCallback() { + return false; + } +} diff --git a/instrumentation/armeria-1.3/testing/src/main/groovy/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpClientTest.groovy b/instrumentation/armeria-1.3/testing/src/main/groovy/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpClientTest.groovy deleted file mode 100644 index b9fe64594c84..000000000000 --- a/instrumentation/armeria-1.3/testing/src/main/groovy/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpClientTest.groovy +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.armeria.v1_3 - -import com.linecorp.armeria.client.WebClient -import com.linecorp.armeria.client.WebClientBuilder -import com.linecorp.armeria.common.HttpMethod -import com.linecorp.armeria.common.HttpRequest -import com.linecorp.armeria.common.RequestHeaders -import io.opentelemetry.api.common.AttributeKey -import io.opentelemetry.instrumentation.test.base.HttpClientTest -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import java.util.concurrent.CompletionException -import spock.lang.Shared - -abstract class AbstractArmeriaHttpClientTest extends HttpClientTest { - - abstract WebClientBuilder configureClient(WebClientBuilder clientBuilder) - - @Shared - def client = configureClient(WebClient.builder()).build() - - @Override - HttpRequest buildRequest(String method, URI uri, Map headers) { - return HttpRequest.of( - RequestHeaders.builder(HttpMethod.valueOf(method), uri.toString()) - .set(headers.entrySet()) - .build()) - } - - @Override - int sendRequest(HttpRequest request, String method, URI uri, Map headers) { - try { - return client.execute(request) - .aggregate() - .join() - .status() - .code() - } catch (CompletionException e) { - throw e.cause - } - } - - @Override - void sendRequestWithCallback(HttpRequest request, String method, URI uri, Map headers, RequestResult requestResult) { - client.execute(request).aggregate().whenComplete {response, throwable -> - requestResult.complete({ response.status().code() }, throwable) - } - } - - // Not supported yet: https://github.com/line/armeria/issues/2489 - @Override - boolean testRedirects() { - false - } - - @Override - boolean testReusedRequest() { - // armeria requests can't be reused - false - } - - @Override - Set> httpAttributes(URI uri) { - Set> extra = [ - SemanticAttributes.HTTP_HOST, - SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, - SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, - SemanticAttributes.HTTP_SCHEME, - SemanticAttributes.HTTP_TARGET - ] - super.httpAttributes(uri) + extra - } -} diff --git a/instrumentation/armeria-1.3/testing/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpClientTest.java b/instrumentation/armeria-1.3/testing/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpClientTest.java new file mode 100644 index 000000000000..8b308bce4373 --- /dev/null +++ b/instrumentation/armeria-1.3/testing/src/main/java/io/opentelemetry/instrumentation/armeria/v1_3/AbstractArmeriaHttpClientTest.java @@ -0,0 +1,96 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.armeria.v1_3; + +import com.linecorp.armeria.client.ClientFactory; +import com.linecorp.armeria.client.WebClient; +import com.linecorp.armeria.client.WebClientBuilder; +import com.linecorp.armeria.common.HttpMethod; +import com.linecorp.armeria.common.HttpRequest; +import com.linecorp.armeria.common.RequestHeaders; +import com.linecorp.armeria.common.util.Exceptions; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.URI; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletionException; +import org.junit.jupiter.api.BeforeEach; + +public abstract class AbstractArmeriaHttpClientTest extends AbstractHttpClientTest { + + protected abstract WebClientBuilder configureClient(WebClientBuilder clientBuilder); + + private WebClient client; + + @BeforeEach + void setupClient() { + client = + configureClient( + WebClient.builder() + .factory(ClientFactory.builder().connectTimeout(connectTimeout()).build())) + .build(); + } + + @Override + protected final HttpRequest buildRequest(String method, URI uri, Map headers) { + return HttpRequest.of( + RequestHeaders.builder(HttpMethod.valueOf(method), uri.toString()) + .set(headers.entrySet()) + .build()); + } + + @Override + protected final int sendRequest( + HttpRequest request, String method, URI uri, Map headers) { + try { + return client.execute(request).aggregate().join().status().code(); + } catch (CompletionException e) { + return Exceptions.throwUnsafely(e.getCause()); + } + } + + @Override + protected final void sendRequestWithCallback( + HttpRequest request, + String method, + URI uri, + Map headers, + RequestResult requestResult) { + client + .execute(request) + .aggregate() + .whenComplete( + (response, throwable) -> + requestResult.complete(() -> response.status().code(), throwable)); + } + + // Not supported yet: https://github.com/line/armeria/issues/2489 + @Override + protected final boolean testRedirects() { + return false; + } + + @Override + protected final boolean testReusedRequest() { + // armeria requests can't be reused + return false; + } + + @Override + protected Set> httpAttributes(URI uri) { + Set> extra = new HashSet<>(); + extra.add(SemanticAttributes.HTTP_HOST); + extra.add(SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH); + extra.add(SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH); + extra.add(SemanticAttributes.HTTP_SCHEME); + extra.add(SemanticAttributes.HTTP_TARGET); + extra.addAll(super.httpAttributes(uri)); + return extra; + } +} diff --git a/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/test/groovy/AsyncHttpClientTest.groovy b/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/test/groovy/AsyncHttpClientTest.groovy index 2364940161b4..cbf906e02fc0 100644 --- a/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/test/groovy/AsyncHttpClientTest.groovy +++ b/instrumentation/async-http-client/async-http-client-1.9/javaagent/src/test/groovy/AsyncHttpClientTest.groovy @@ -12,7 +12,8 @@ import com.ning.http.client.Response import com.ning.http.client.uri.Uri import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpClientTest -import io.opentelemetry.instrumentation.test.base.SingleConnection +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection import spock.lang.AutoCleanup import spock.lang.Shared @@ -39,7 +40,7 @@ class AsyncHttpClientTest extends HttpClientTest implements AgentTestTr } @Override - void sendRequestWithCallback(Request request, String method, URI uri, Map headers, RequestResult requestResult) { + void sendRequestWithCallback(Request request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { // TODO(anuraaga): Do we also need to test ListenableFuture callback? client.executeRequest(request, new AsyncCompletionHandler() { @Override diff --git a/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/test/groovy/AsyncHttpClientTest.groovy b/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/test/groovy/AsyncHttpClientTest.groovy index d1fb9d1d60f2..d46720e72c2a 100644 --- a/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/test/groovy/AsyncHttpClientTest.groovy +++ b/instrumentation/async-http-client/async-http-client-2.0/javaagent/src/test/groovy/AsyncHttpClientTest.groovy @@ -6,6 +6,7 @@ import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest import io.opentelemetry.semconv.trace.attributes.SemanticAttributes import org.asynchttpclient.AsyncCompletionHandler import org.asynchttpclient.Dsl @@ -40,7 +41,7 @@ class AsyncHttpClientTest extends HttpClientTest implements AgentTestTr } @Override - void sendRequestWithCallback(Request request, String method, URI uri, Map headers, RequestResult requestResult) { + void sendRequestWithCallback(Request request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { client.executeRequest(request, new AsyncCompletionHandler() { @Override Void onCompleted(Response response) throws Exception { diff --git a/instrumentation/http-url-connection/javaagent/src/test/groovy/SpringRestTemplateTest.groovy b/instrumentation/http-url-connection/javaagent/src/test/groovy/SpringRestTemplateTest.groovy index cba4b37eb9cf..2c11d38c113e 100644 --- a/instrumentation/http-url-connection/javaagent/src/test/groovy/SpringRestTemplateTest.groovy +++ b/instrumentation/http-url-connection/javaagent/src/test/groovy/SpringRestTemplateTest.groovy @@ -5,6 +5,7 @@ import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest import org.springframework.http.HttpEntity import org.springframework.http.HttpHeaders import org.springframework.http.HttpMethod @@ -44,7 +45,7 @@ class SpringRestTemplateTest extends HttpClientTest> implemen } @Override - void sendRequestWithCallback(HttpEntity request, String method, URI uri, Map headers = [:], RequestResult requestResult) { + void sendRequestWithCallback(HttpEntity request, String method, URI uri, Map headers = [:], AbstractHttpClientTest.RequestResult requestResult) { try { restTemplate.execute(uri, HttpMethod.valueOf(method), { req -> req.getHeaders().putAll(request.getHeaders()) diff --git a/instrumentation/java-http-client/javaagent/src/test/groovy/JdkHttpClientTest.groovy b/instrumentation/java-http-client/javaagent/src/test/groovy/JdkHttpClientTest.groovy index 9ecf6989ba5e..20053b8f54ff 100644 --- a/instrumentation/java-http-client/javaagent/src/test/groovy/JdkHttpClientTest.groovy +++ b/instrumentation/java-http-client/javaagent/src/test/groovy/JdkHttpClientTest.groovy @@ -5,6 +5,7 @@ import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpResponse @@ -38,7 +39,7 @@ class JdkHttpClientTest extends HttpClientTest implements AgentTest } @Override - void sendRequestWithCallback(HttpRequest request, String method, URI uri, Map headers, RequestResult requestResult) { + void sendRequestWithCallback(HttpRequest request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .whenComplete { response, throwable -> requestResult.complete({ response.statusCode() }, throwable?.getCause()) diff --git a/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/test/groovy/JaxRsClientTest.groovy b/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/test/groovy/JaxRsClientTest.groovy index 5f7e75cf5828..6def42601d1a 100644 --- a/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/test/groovy/JaxRsClientTest.groovy +++ b/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/test/groovy/JaxRsClientTest.groovy @@ -9,7 +9,8 @@ import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTr import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpClientTest -import io.opentelemetry.instrumentation.test.base.SingleConnection +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection import io.opentelemetry.semconv.trace.attributes.SemanticAttributes import java.util.concurrent.TimeUnit import javax.ws.rs.ProcessingException @@ -46,7 +47,7 @@ abstract class JaxRsClientTest extends HttpClientTest implem } @Override - void sendRequestWithCallback(Invocation.Builder request, String method, URI uri, Map headers, RequestResult requestResult) { + void sendRequestWithCallback(Invocation.Builder request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { def body = BODY_METHODS.contains(method) ? Entity.text("") : null request.async().method(method, (Entity) body, new InvocationCallback() { diff --git a/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/test/groovy/ResteasySingleConnection.groovy b/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/test/groovy/ResteasySingleConnection.groovy index ab220cc9d3ec..665d9a7e5387 100644 --- a/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/test/groovy/ResteasySingleConnection.groovy +++ b/instrumentation/jaxrs-client/jaxrs-client-2.0/jaxrs-client-2.0-common/javaagent/src/test/groovy/ResteasySingleConnection.groovy @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import io.opentelemetry.instrumentation.test.base.SingleConnection +import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection import java.util.concurrent.ExecutionException import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException diff --git a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/testing/src/main/groovy/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/AbstractJettyClient9Test.groovy b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/testing/src/main/groovy/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/AbstractJettyClient9Test.groovy index 7a24b5899773..671a4d0b9ed8 100644 --- a/instrumentation/jetty-httpclient/jetty-httpclient-9.2/testing/src/main/groovy/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/AbstractJettyClient9Test.groovy +++ b/instrumentation/jetty-httpclient/jetty-httpclient-9.2/testing/src/main/groovy/io/opentelemetry/instrumentation/jetty/httpclient/v9_2/AbstractJettyClient9Test.groovy @@ -7,7 +7,9 @@ package io.opentelemetry.instrumentation.jetty.httpclient.v9_2 import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import java.util.concurrent.TimeUnit import org.eclipse.jetty.client.HttpClient import org.eclipse.jetty.client.api.ContentResponse import org.eclipse.jetty.client.api.Request @@ -15,12 +17,8 @@ import org.eclipse.jetty.client.api.Response import org.eclipse.jetty.client.api.Result import org.eclipse.jetty.http.HttpMethod import org.eclipse.jetty.util.ssl.SslContextFactory -import org.junit.Rule -import org.junit.rules.TestName import spock.lang.Shared -import java.util.concurrent.TimeUnit - abstract class AbstractJettyClient9Test extends HttpClientTest { abstract HttpClient createStandardClient() @@ -33,9 +31,6 @@ abstract class AbstractJettyClient9Test extends HttpClientTest { @Shared def httpsClient = null - @Rule - TestName name = new TestName() - Request jettyRequest = null def setupSpec() { @@ -55,6 +50,7 @@ abstract class AbstractJettyClient9Test extends HttpClientTest { HttpClient theClient = uri.scheme == 'https' ? httpsClient : client Request request = theClient.newRequest(uri) + request.agent("Jetty") HttpMethod methodObj = HttpMethod.valueOf(method) request.method(methodObj) @@ -67,9 +63,6 @@ abstract class AbstractJettyClient9Test extends HttpClientTest { @Override String userAgent() { - if (name.methodName.startsWith('connection error') && jettyRequest.getAgent() == null) { - return null - } return "Jetty" } @@ -102,7 +95,7 @@ abstract class AbstractJettyClient9Test extends HttpClientTest { } @Override - void sendRequestWithCallback(Request request, String method, URI uri, Map headers, RequestResult requestResult) { + void sendRequestWithCallback(Request request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { JettyClientListener jcl = new JettyClientListener() diff --git a/instrumentation/netty/netty-3.8/javaagent/src/test/groovy/Netty38ClientTest.groovy b/instrumentation/netty/netty-3.8/javaagent/src/test/groovy/Netty38ClientTest.groovy index 46eb792188ce..78ed5dc481fe 100644 --- a/instrumentation/netty/netty-3.8/javaagent/src/test/groovy/Netty38ClientTest.groovy +++ b/instrumentation/netty/netty-3.8/javaagent/src/test/groovy/Netty38ClientTest.groovy @@ -11,8 +11,8 @@ import com.ning.http.client.RequestBuilder import com.ning.http.client.Response import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.instrumentation.test.AgentTestTrait -import io.opentelemetry.instrumentation.test.asserts.SpanAssert import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest import java.nio.channels.ClosedChannelException import spock.lang.AutoCleanup import spock.lang.Shared @@ -62,7 +62,7 @@ class Netty38ClientTest extends HttpClientTest implements AgentTestTrai } @Override - void sendRequestWithCallback(Request request, String method, URI uri, Map headers, RequestResult requestResult) { + void sendRequestWithCallback(Request request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { // TODO(anuraaga): Do we also need to test ListenableFuture callback? client.executeRequest(request, new AsyncCompletionHandler() { @Override @@ -95,7 +95,7 @@ class Netty38ClientTest extends HttpClientTest implements AgentTestTrai } @Override - void assertClientSpanErrorEvent(SpanAssert spanAssert, URI uri, Throwable exception) { + Throwable clientSpanError(URI uri, Throwable exception) { switch (uri.toString()) { case "http://localhost:61/": // unopened port exception = exception.getCause() != null ? exception.getCause() : new ConnectException("Connection refused: localhost/127.0.0.1:61") @@ -103,7 +103,7 @@ class Netty38ClientTest extends HttpClientTest implements AgentTestTrai case "https://192.0.2.1/": // non routable address exception = exception.getCause() != null ? exception.getCause() : new ClosedChannelException() } - super.assertClientSpanErrorEvent(spanAssert, uri, exception) + return exception } @Override diff --git a/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ClientTest.groovy b/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ClientTest.groovy index 3ff3559b0d29..f5caa10f65c2 100644 --- a/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ClientTest.groovy +++ b/instrumentation/netty/netty-4.0/javaagent/src/test/groovy/Netty40ClientTest.groovy @@ -22,6 +22,7 @@ import io.netty.handler.timeout.ReadTimeoutHandler import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit import spock.lang.Shared @@ -86,7 +87,7 @@ class Netty40ClientTest extends HttpClientTest implement } @Override - void sendRequestWithCallback(DefaultFullHttpRequest request, String method, URI uri, Map headers, RequestResult requestResult) { + void sendRequestWithCallback(DefaultFullHttpRequest request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { Channel ch try { ch = getBootstrap(uri).connect(uri.host, getPort(uri)).sync().channel() diff --git a/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/Netty41ClientTest.groovy b/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/Netty41ClientTest.groovy index 71c64b15753f..d0dae525e92c 100644 --- a/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/Netty41ClientTest.groovy +++ b/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/Netty41ClientTest.groovy @@ -30,7 +30,8 @@ import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.api.trace.SpanKind import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpClientTest -import io.opentelemetry.instrumentation.test.base.SingleConnection +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection import io.opentelemetry.javaagent.instrumentation.netty.v4_1.client.HttpClientTracingHandler import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit @@ -114,7 +115,7 @@ class Netty41ClientTest extends HttpClientTest implement } @Override - void sendRequestWithCallback(DefaultFullHttpRequest request, String method, URI uri, Map headers, RequestResult requestResult) { + void sendRequestWithCallback(DefaultFullHttpRequest request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { Channel ch try { ch = getBootstrap(uri).connect(uri.host, getPort(uri)).sync().channel() diff --git a/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/SingleNettyConnection.java b/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/SingleNettyConnection.java index eb670a76ab7a..a90730a22afb 100644 --- a/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/SingleNettyConnection.java +++ b/instrumentation/netty/netty-4.1/javaagent/src/test/groovy/SingleNettyConnection.java @@ -20,7 +20,7 @@ import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpVersion; -import io.opentelemetry.instrumentation.test.base.SingleConnection; +import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.Map; diff --git a/instrumentation/okhttp/okhttp-2.2/javaagent/src/test/groovy/OkHttp2Test.groovy b/instrumentation/okhttp/okhttp-2.2/javaagent/src/test/groovy/OkHttp2Test.groovy index bb22ebfb918b..2682ad4b1076 100644 --- a/instrumentation/okhttp/okhttp-2.2/javaagent/src/test/groovy/OkHttp2Test.groovy +++ b/instrumentation/okhttp/okhttp-2.2/javaagent/src/test/groovy/OkHttp2Test.groovy @@ -12,6 +12,7 @@ import com.squareup.okhttp.Response import com.squareup.okhttp.internal.http.HttpMethod import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest import java.util.concurrent.TimeUnit import spock.lang.Shared @@ -39,7 +40,7 @@ class OkHttp2Test extends HttpClientTest implements AgentTestTrait { } @Override - void sendRequestWithCallback(Request request, String method, URI uri, Map headers, RequestResult requestResult) { + void sendRequestWithCallback(Request request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { client.newCall(request).enqueue(new Callback() { @Override void onFailure(Request req, IOException e) { diff --git a/instrumentation/okhttp/okhttp-3.0/testing/src/main/groovy/io/opentelemetry/instrumentation/okhttp/v3_0/AbstractOkHttp3Test.groovy b/instrumentation/okhttp/okhttp-3.0/testing/src/main/groovy/io/opentelemetry/instrumentation/okhttp/v3_0/AbstractOkHttp3Test.groovy index fde3bda8a8f8..996ddf89747f 100644 --- a/instrumentation/okhttp/okhttp-3.0/testing/src/main/groovy/io/opentelemetry/instrumentation/okhttp/v3_0/AbstractOkHttp3Test.groovy +++ b/instrumentation/okhttp/okhttp-3.0/testing/src/main/groovy/io/opentelemetry/instrumentation/okhttp/v3_0/AbstractOkHttp3Test.groovy @@ -7,6 +7,7 @@ package io.opentelemetry.instrumentation.okhttp.v3_0 import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest import io.opentelemetry.semconv.trace.attributes.SemanticAttributes import java.util.concurrent.TimeUnit import okhttp3.Call @@ -48,7 +49,7 @@ abstract class AbstractOkHttp3Test extends HttpClientTest { } @Override - void sendRequestWithCallback(Request request, String method, URI uri, Map headers, RequestResult requestResult) { + void sendRequestWithCallback(Request request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { client.newCall(request).enqueue(new Callback() { @Override void onFailure(Call call, IOException e) { diff --git a/instrumentation/play-ws/play-ws-testing/src/main/groovy/PlayWsClientTestBase.groovy b/instrumentation/play-ws/play-ws-testing/src/main/groovy/PlayWsClientTestBase.groovy index 5d44c11f2be2..5d33bc7f4888 100644 --- a/instrumentation/play-ws/play-ws-testing/src/main/groovy/PlayWsClientTestBase.groovy +++ b/instrumentation/play-ws/play-ws-testing/src/main/groovy/PlayWsClientTestBase.groovy @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest import java.util.concurrent.CompletionStage import java.util.concurrent.TimeUnit import play.libs.ws.StandaloneWSClient @@ -36,7 +36,7 @@ class PlayJavaWsClientTestBase extends PlayWsClientTestBaseBase headers, HttpClientTest.RequestResult requestResult) { + void sendRequestWithCallback(StandaloneWSRequest request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { request.execute().whenComplete { response, throwable -> requestResult.complete({ response.status }, throwable) } @@ -69,7 +69,7 @@ class PlayJavaStreamedWsClientTestBase extends PlayWsClientTestBaseBase headers, HttpClientTest.RequestResult requestResult) { + void sendRequestWithCallback(StandaloneWSRequest request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { internalSendRequest(request).whenComplete { response, throwable -> requestResult.complete({ response.status }, throwable?.getCause()) } @@ -120,7 +120,7 @@ class PlayScalaWsClientTestBase extends PlayWsClientTestBaseBase headers, HttpClientTest.RequestResult requestResult) { + void sendRequestWithCallback(play.api.libs.ws.StandaloneWSRequest request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { request.execute().onComplete(new Function1, Void>() { @Override Void apply(Try response) { @@ -161,7 +161,7 @@ class PlayScalaStreamedWsClientTestBase extends PlayWsClientTestBaseBase headers, HttpClientTest.RequestResult requestResult) { + void sendRequestWithCallback(play.api.libs.ws.StandaloneWSRequest request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { internalSendRequest(request).onComplete(new Function1, Void>() { @Override Void apply(Try response) { diff --git a/instrumentation/play/play-2.4/javaagent/src/test/groovy/client/PlayWsClientTest.groovy b/instrumentation/play/play-2.4/javaagent/src/test/groovy/client/PlayWsClientTest.groovy index c7733116a486..a1d6f432e672 100644 --- a/instrumentation/play/play-2.4/javaagent/src/test/groovy/client/PlayWsClientTest.groovy +++ b/instrumentation/play/play-2.4/javaagent/src/test/groovy/client/PlayWsClientTest.groovy @@ -8,7 +8,8 @@ package client import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpClientTest -import io.opentelemetry.instrumentation.test.base.SingleConnection +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection import io.opentelemetry.semconv.trace.attributes.SemanticAttributes import java.util.concurrent.CompletionStage import play.libs.ws.WS @@ -41,7 +42,7 @@ class PlayWsClientTest extends HttpClientTest implements AgentTestTra } @Override - void sendRequestWithCallback(WSRequest request, String method, URI uri, Map headers, RequestResult requestResult) { + void sendRequestWithCallback(WSRequest request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { internalSendRequest(request, method).whenComplete { response, throwable -> requestResult.complete({ response.status }, throwable) } diff --git a/instrumentation/ratpack-1.4/javaagent/src/test/groovy/client/RatpackHttpClientTest.groovy b/instrumentation/ratpack-1.4/javaagent/src/test/groovy/client/RatpackHttpClientTest.groovy index 8075998458c2..848b0ceb466b 100644 --- a/instrumentation/ratpack-1.4/javaagent/src/test/groovy/client/RatpackHttpClientTest.groovy +++ b/instrumentation/ratpack-1.4/javaagent/src/test/groovy/client/RatpackHttpClientTest.groovy @@ -9,9 +9,9 @@ import io.netty.channel.ConnectTimeoutException import io.netty.handler.timeout.ReadTimeoutException import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.instrumentation.test.AgentTestTrait -import io.opentelemetry.instrumentation.test.asserts.SpanAssert import io.opentelemetry.instrumentation.test.base.HttpClientTest -import io.opentelemetry.instrumentation.test.base.SingleConnection +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection import java.time.Duration import java.util.concurrent.ExecutionException import java.util.concurrent.TimeoutException @@ -69,7 +69,7 @@ class RatpackHttpClientTest extends HttpClientTest implements AgentTestTra } @Override - void sendRequestWithCallback(Void request, String method, URI uri, Map headers, RequestResult requestResult) { + void sendRequestWithCallback(Void request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { exec.execute(Operation.of { internalSendRequest(client, method, uri, headers).result {result -> requestResult.complete({ result.value }, result.throwable) @@ -122,16 +122,13 @@ class RatpackHttpClientTest extends HttpClientTest implements AgentTestTra } @Override - void assertClientSpanErrorEvent(SpanAssert spanAssert, URI uri, Throwable exception) { - // non routable address + Throwable clientSpanError(URI uri, Throwable exception) { if (uri.toString() == "https://192.0.2.1/") { - spanAssert.errorEvent(ConnectTimeoutException, ~/connection timed out:/) - return + return new ConnectTimeoutException("connection timed out: /192.0.2.1:443") } else if (uri.getPath() == "/read-timeout") { - spanAssert.errorEvent(ReadTimeoutException) - return + return new ReadTimeoutException() } - super.assertClientSpanErrorEvent(spanAssert, uri, exception) + return exception } @Override diff --git a/instrumentation/ratpack-1.4/javaagent/src/test/groovy/client/RatpackPooledHttpClientTest.groovy b/instrumentation/ratpack-1.4/javaagent/src/test/groovy/client/RatpackPooledHttpClientTest.groovy index 4ee09f919197..cb2b8babb258 100644 --- a/instrumentation/ratpack-1.4/javaagent/src/test/groovy/client/RatpackPooledHttpClientTest.groovy +++ b/instrumentation/ratpack-1.4/javaagent/src/test/groovy/client/RatpackPooledHttpClientTest.groovy @@ -5,7 +5,7 @@ package client -import io.opentelemetry.instrumentation.test.base.SingleConnection +import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection import ratpack.http.client.HttpClient class RatpackPooledHttpClientTest extends RatpackHttpClientTest { diff --git a/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/AbstractReactorNettyHttpClientTest.groovy b/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/AbstractReactorNettyHttpClientTest.groovy index a569adb54c2a..67c403a6a1f0 100644 --- a/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/AbstractReactorNettyHttpClientTest.groovy +++ b/instrumentation/reactor-netty/reactor-netty-0.9/javaagent/src/test/groovy/io/opentelemetry/javaagent/instrumentation/reactornetty/v0_9/AbstractReactorNettyHttpClientTest.groovy @@ -12,8 +12,8 @@ import io.opentelemetry.api.trace.Span import io.opentelemetry.api.trace.SpanKind import io.opentelemetry.api.trace.StatusCode import io.opentelemetry.instrumentation.test.AgentTestTrait -import io.opentelemetry.instrumentation.test.asserts.SpanAssert import io.opentelemetry.instrumentation.test.base.HttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest import io.opentelemetry.sdk.trace.data.SpanData import java.util.concurrent.atomic.AtomicReference import reactor.netty.http.client.HttpClient @@ -51,7 +51,7 @@ abstract class AbstractReactorNettyHttpClientTest extends HttpClientTest headers, RequestResult requestResult) { + void sendRequestWithCallback(HttpClient.ResponseReceiver request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { request.responseSingle {resp, content -> // Make sure to consume content since that's when we close the span. content.map { resp } @@ -74,7 +74,7 @@ abstract class AbstractReactorNettyHttpClientTest extends HttpClientTest headers, RequestResult requestResult) { + void sendRequestWithCallback(HttpClient.ResponseReceiver request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { request.responseSingle {resp, content -> // Make sure to consume content since that's when we close the span. content.map { resp } @@ -79,7 +79,7 @@ abstract class AbstractReactorNettyHttpClientTest extends HttpClientTest> } @Override - void sendRequestWithCallback(HttpEntity request, String method, URI uri, Map headers, RequestResult requestResult) { + void sendRequestWithCallback(HttpEntity request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { try { restTemplate.execute(uri, HttpMethod.valueOf(method), { req -> headers.forEach(req.getHeaders().&add) diff --git a/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/client/SpringWebFluxSingleConnection.groovy b/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/client/SpringWebFluxSingleConnection.groovy index 2a6cf5c95ca4..7d10836bae0a 100644 --- a/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/client/SpringWebFluxSingleConnection.groovy +++ b/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/client/SpringWebFluxSingleConnection.groovy @@ -7,7 +7,7 @@ package client import io.netty.channel.ChannelOption import io.opentelemetry.instrumentation.test.base.HttpClientTest -import io.opentelemetry.instrumentation.test.base.SingleConnection +import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection import java.util.concurrent.ExecutionException import java.util.concurrent.TimeoutException import org.springframework.http.HttpMethod diff --git a/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/client/SpringWebfluxHttpClientTest.groovy b/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/client/SpringWebfluxHttpClientTest.groovy index 4fe8e643baf5..118bb3ef52f5 100644 --- a/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/client/SpringWebfluxHttpClientTest.groovy +++ b/instrumentation/spring/spring-webflux-5.0/javaagent/src/test/groovy/client/SpringWebfluxHttpClientTest.groovy @@ -7,9 +7,9 @@ package client import io.netty.channel.ChannelOption import io.opentelemetry.instrumentation.test.AgentTestTrait -import io.opentelemetry.instrumentation.test.asserts.SpanAssert import io.opentelemetry.instrumentation.test.base.HttpClientTest -import io.opentelemetry.instrumentation.test.base.SingleConnection +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection import org.springframework.http.HttpMethod import org.springframework.http.client.reactive.ReactorClientHttpConnector import org.springframework.web.reactive.function.client.WebClient @@ -49,7 +49,7 @@ class SpringWebfluxHttpClientTest extends HttpClientTest headers, RequestResult requestResult) { + void sendRequestWithCallback(WebClient.RequestBodySpec request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { request.exchange().subscribe({ requestResult.complete(it.statusCode().value()) }, { @@ -58,7 +58,7 @@ class SpringWebfluxHttpClientTest extends HttpClientTest implements A } @Override - void sendRequestWithCallback(HttpClientRequest request, String method, URI uri, Map headers, RequestResult requestResult) { + void sendRequestWithCallback(HttpClientRequest request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { sendRequest(request).whenComplete { status, throwable -> requestResult.complete({ status }, throwable) } diff --git a/instrumentation/vertx-http-client-3.0/javaagent/src/test/groovy/client/VertxSingleConnection.java b/instrumentation/vertx-http-client-3.0/javaagent/src/test/groovy/client/VertxSingleConnection.java index 67ef56e9fff8..f3d9d47bc3d1 100644 --- a/instrumentation/vertx-http-client-3.0/javaagent/src/test/groovy/client/VertxSingleConnection.java +++ b/instrumentation/vertx-http-client-3.0/javaagent/src/test/groovy/client/VertxSingleConnection.java @@ -5,7 +5,7 @@ package client; -import io.opentelemetry.instrumentation.test.base.SingleConnection; +import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection; import io.vertx.core.Vertx; import io.vertx.core.VertxOptions; import io.vertx.core.http.HttpClient; diff --git a/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/client/VertxRxCircuitBreakerWebClientTest.groovy b/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/client/VertxRxCircuitBreakerWebClientTest.groovy index 570180028af8..f3343367b6ce 100644 --- a/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/client/VertxRxCircuitBreakerWebClientTest.groovy +++ b/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/client/VertxRxCircuitBreakerWebClientTest.groovy @@ -7,7 +7,8 @@ package client import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.base.HttpClientTest -import io.opentelemetry.instrumentation.test.base.SingleConnection +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection import io.vertx.circuitbreaker.CircuitBreakerOptions import io.vertx.core.AsyncResult import io.vertx.core.VertxOptions @@ -70,7 +71,7 @@ class VertxRxCircuitBreakerWebClientTest extends HttpClientTest> } @Override - void sendRequestWithCallback(HttpRequest request, String method, URI uri, Map headers, RequestResult requestResult) { + void sendRequestWithCallback(HttpRequest request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { sendRequestWithCallback(request) { if (it.succeeded()) { requestResult.complete(it.result().statusCode()) diff --git a/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/client/VertxRxWebClientTest.groovy b/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/client/VertxRxWebClientTest.groovy index 1efc221b9419..b7c229a4bb51 100644 --- a/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/client/VertxRxWebClientTest.groovy +++ b/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/client/VertxRxWebClientTest.groovy @@ -6,9 +6,9 @@ package client import io.opentelemetry.instrumentation.test.AgentTestTrait -import io.opentelemetry.instrumentation.test.asserts.SpanAssert import io.opentelemetry.instrumentation.test.base.HttpClientTest -import io.opentelemetry.instrumentation.test.base.SingleConnection +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection import io.vertx.core.VertxOptions import io.vertx.core.http.HttpMethod import io.vertx.ext.web.client.WebClientOptions @@ -41,7 +41,7 @@ class VertxRxWebClientTest extends HttpClientTest> implement } @Override - void sendRequestWithCallback(HttpRequest request, String method, URI uri, Map headers, RequestResult requestResult) { + void sendRequestWithCallback(HttpRequest request, String method, URI uri, Map headers, AbstractHttpClientTest.RequestResult requestResult) { request.rxSend() .subscribe(new io.reactivex.functions.Consumer>() { @Override @@ -57,7 +57,7 @@ class VertxRxWebClientTest extends HttpClientTest> implement } @Override - void assertClientSpanErrorEvent(SpanAssert spanAssert, URI uri, Throwable exception) { + Throwable clientSpanError(URI uri, Throwable exception) { if (exception.class == RuntimeException) { switch (uri.toString()) { case "http://localhost:61/": // unopened port @@ -65,7 +65,7 @@ class VertxRxWebClientTest extends HttpClientTest> implement exception = exception.getCause() } } - super.assertClientSpanErrorEvent(spanAssert, uri, exception) + return exception } @Override diff --git a/instrumentation/vertx-reactive-3.5/javaagent/src/test/java/client/VertxRxSingleConnection.java b/instrumentation/vertx-reactive-3.5/javaagent/src/test/java/client/VertxRxSingleConnection.java index c5a1d9edc671..96ee230533a4 100644 --- a/instrumentation/vertx-reactive-3.5/javaagent/src/test/java/client/VertxRxSingleConnection.java +++ b/instrumentation/vertx-reactive-3.5/javaagent/src/test/java/client/VertxRxSingleConnection.java @@ -5,7 +5,7 @@ package client; -import io.opentelemetry.instrumentation.test.base.SingleConnection; +import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection; import io.vertx.core.VertxOptions; import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.client.WebClientOptions; diff --git a/testing-common/build.gradle.kts b/testing-common/build.gradle.kts index 73425d55913d..383eac3237d7 100644 --- a/testing-common/build.gradle.kts +++ b/testing-common/build.gradle.kts @@ -16,7 +16,8 @@ sourceSets { dependencies { api("org.codehaus.groovy:groovy-all") api("org.spockframework:spock-core") - implementation("org.junit.jupiter:junit-jupiter-api") + api("org.junit.jupiter:junit-jupiter-api") + api("org.junit.jupiter:junit-jupiter-params") api("io.opentelemetry:opentelemetry-api") api("io.opentelemetry:opentelemetry-semconv") @@ -27,6 +28,9 @@ dependencies { api("org.assertj:assertj-core") + // Needs to be api dependency due to Spock restriction. + api("org.awaitility:awaitility") + compileOnly(project(path = ":testing:armeria-shaded-for-testing", configuration = "shadow")) implementation("io.opentelemetry:opentelemetry-proto") { @@ -39,7 +43,6 @@ dependencies { implementation("net.bytebuddy:byte-buddy-agent") implementation("org.slf4j:slf4j-api") implementation("ch.qos.logback:logback-classic") - implementation("org.awaitility:awaitility") implementation("org.slf4j:log4j-over-slf4j") implementation("org.slf4j:jcl-over-slf4j") implementation("org.slf4j:jul-to-slf4j") diff --git a/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpClientTest.groovy b/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpClientTest.groovy index 174293c474dd..9495756ab7f2 100644 --- a/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpClientTest.groovy +++ b/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpClientTest.groovy @@ -5,51 +5,18 @@ package io.opentelemetry.instrumentation.test.base -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.SERVER -import static io.opentelemetry.api.trace.StatusCode.ERROR -import static io.opentelemetry.instrumentation.test.utils.PortUtils.UNUSABLE_PORT -import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderParentClientSpan -import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP -import static io.opentelemetry.testing.internal.armeria.common.MediaType.PLAIN_TEXT_UTF_8 import static org.junit.Assume.assumeTrue -import groovy.transform.stc.ClosureParams -import groovy.transform.stc.SimpleType -import io.opentelemetry.api.GlobalOpenTelemetry import io.opentelemetry.api.common.AttributeKey -import io.opentelemetry.api.trace.Span -import io.opentelemetry.api.trace.SpanBuilder -import io.opentelemetry.api.trace.SpanKind -import io.opentelemetry.api.trace.Tracer -import io.opentelemetry.context.Context +import io.opentelemetry.api.trace.SpanId import io.opentelemetry.instrumentation.test.InstrumentationSpecification -import io.opentelemetry.instrumentation.test.asserts.AttributesAssert -import io.opentelemetry.instrumentation.test.asserts.SpanAssert import io.opentelemetry.instrumentation.test.asserts.TraceAssert -import io.opentelemetry.instrumentation.test.server.http.RequestContextGetter +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestServer +import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection +import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions import io.opentelemetry.sdk.trace.data.SpanData import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import io.opentelemetry.testing.internal.armeria.common.HttpData -import io.opentelemetry.testing.internal.armeria.common.HttpRequest -import io.opentelemetry.testing.internal.armeria.common.HttpResponse -import io.opentelemetry.testing.internal.armeria.common.HttpStatus -import io.opentelemetry.testing.internal.armeria.common.ResponseHeaders -import io.opentelemetry.testing.internal.armeria.common.ResponseHeadersBuilder -import io.opentelemetry.testing.internal.armeria.server.DecoratingHttpServiceFunction -import io.opentelemetry.testing.internal.armeria.server.HttpService -import io.opentelemetry.testing.internal.armeria.server.ServerBuilder -import io.opentelemetry.testing.internal.armeria.server.ServiceRequestContext -import io.opentelemetry.testing.internal.armeria.server.logging.LoggingService -import io.opentelemetry.testing.internal.armeria.testing.junit5.server.ServerExtension -import java.security.KeyStore -import java.util.concurrent.CountDownLatch -import java.util.concurrent.ExecutionException -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException -import java.util.function.Supplier -import javax.net.ssl.KeyManagerFactory import spock.lang.Requires import spock.lang.Shared import spock.lang.Unroll @@ -59,167 +26,6 @@ abstract class HttpClientTest extends InstrumentationSpecification { protected static final BODY_METHODS = ["POST", "PUT"] protected static final CONNECT_TIMEOUT_MS = 5000 protected static final READ_TIMEOUT_MS = 2000 - protected static final BASIC_AUTH_KEY = "custom-authorization-header" - protected static final BASIC_AUTH_VAL = "plain text auth token" - - @Shared - Tracer tracer = openTelemetry.getTracer("test") - - @Shared - def server= new ServerExtension(false) { - @Override - protected void configure(ServerBuilder sb) throws Exception { - KeyStore keystore = KeyStore.getInstance("PKCS12") - keystore.load(new FileInputStream(new File(System.getProperty("javax.net.ssl.trustStore"))), "testing".toCharArray()) - KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) - kmf.init(keystore, "testing".toCharArray()) - - sb.http(0) - .https(0) - .tls(kmf) - .service("/success") {ctx, req -> - ResponseHeadersBuilder headers = ResponseHeaders.builder(HttpStatus.OK) - def testRequestId = req.headers().get("test-request-id") - if (testRequestId != null) { - headers.set("test-request-id", testRequestId) - } - HttpResponse.of(headers.build(), HttpData.ofAscii("Hello.")) - } - .service("/client-error") {ctx, req -> - HttpResponse.of(HttpStatus.BAD_REQUEST, PLAIN_TEXT_UTF_8, "Invalid RQ") - } - .service("/error") {ctx, req -> - HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, PLAIN_TEXT_UTF_8, "Sorry.") - } - .service("/redirect") {ctx, req -> - HttpResponse.ofRedirect(HttpStatus.FOUND, "/success") - } - .service("/another-redirect") {ctx, req -> - HttpResponse.ofRedirect(HttpStatus.FOUND, "/redirect") - } - .service("/circular-redirect") {ctx, req -> - HttpResponse.ofRedirect(HttpStatus.FOUND, "/circular-redirect") - } - .service("/secured") {ctx, req -> - if (req.headers().get(BASIC_AUTH_KEY) == BASIC_AUTH_VAL) { - return HttpResponse.of(HttpStatus.OK, PLAIN_TEXT_UTF_8, "secured string under basic auth") - } - return HttpResponse.of(HttpStatus.UNAUTHORIZED, PLAIN_TEXT_UTF_8, "Unauthorized") - } - .service("/to-secured") {ctx, req -> - HttpResponse.ofRedirect(HttpStatus.FOUND, "/secured") - } - .service("/read-timeout") {ctx, req -> - Thread.sleep(READ_TIMEOUT_MS * 5) - ResponseHeadersBuilder headers = ResponseHeaders.builder(HttpStatus.OK) - HttpResponse.of(headers.build(), HttpData.ofAscii("Should have timed out.")) - } - .decorator(new DecoratingHttpServiceFunction() { - @Override - HttpResponse serve(HttpService delegate, ServiceRequestContext ctx, HttpRequest req) { - for (String field : openTelemetry.propagators.textMapPropagator.fields()) { - if (req.headers().getAll(field).size() > 1) { - throw new AssertionError((Object) ("more than one " + field + " header present")) - } - } - SpanBuilder span = tracer.spanBuilder("test-http-server") - .setSpanKind(SERVER) - .setParent(openTelemetry.propagators.textMapPropagator.extract(Context.current(), ctx, RequestContextGetter.INSTANCE)) - - def traceRequestId = req.headers().get("test-request-id") - if (traceRequestId != null) { - span.setAttribute("test.request.id", Integer.parseInt(traceRequestId)) - } - span.startSpan().end() - - return delegate.serve(ctx, req) - } - }) - .decorator(LoggingService.newDecorator()) - } - } - - def setupSpec() { - server.start() - } - - def cleanupSpec() { - server.stop() - } - - // ideally private, but then groovy closures in this class cannot find them - final int doRequest(String method, URI uri, Map headers = [:]) { - def request = buildRequest(method, uri, headers) - return sendRequest(request, method, uri, headers) - } - - private int doReusedRequest(String method, URI uri) { - def request = buildRequest(method, uri, [:]) - sendRequest(request, method, uri, [:]) - return sendRequest(request, method, uri, [:]) - } - - private int doRequestWithExistingTracingHeaders(String method, URI uri) { - def headers = new HashMap() - for (String field : GlobalOpenTelemetry.getPropagators().getTextMapPropagator().fields()) { - headers.put(field, "12345789") - } - def request = buildRequest(method, uri, headers) - return sendRequest(request, method, uri, headers) - } - - // ideally private, but then groovy closures in this class cannot find them - final RequestResult doRequestWithCallback(String method, URI uri, Map headers = [:], - Runnable callback) { - def request = buildRequest(method, uri, headers) - def requestResult = new RequestResult(callback) - sendRequestWithCallback(request, method, uri, headers, requestResult) - return requestResult - } - - /** - * Helper class for capturing result of asynchronous request and running a callback when result - * is received. - */ - static class RequestResult { - private static final long timeout = 10_000 - private final CountDownLatch valueReady = new CountDownLatch(1) - private final Runnable callback - private int status - private Throwable throwable - - RequestResult(Runnable callback) { - this.callback = callback - } - - void complete(int status) { - complete({ status }, null) - } - - void complete(Throwable throwable) { - complete(null, throwable) - } - - void complete(Supplier status, Throwable throwable) { - if (throwable != null) { - this.throwable = throwable - } else { - this.status = status.get() - } - callback.run() - valueReady.countDown() - } - - int get() { - if (!valueReady.await(timeout, TimeUnit.MILLISECONDS)) { - throw new TimeoutException("Timed out waiting for response in " + timeout + "ms") - } - if (throwable != null) { - throw throwable - } - return status - } - } /** * Build the request to be passed to @@ -288,339 +94,233 @@ abstract class HttpClientTest extends InstrumentationSpecification { * and instead, {@link #testCallback} should be implemented to return false. */ void sendRequestWithCallback(REQUEST request, String method, URI uri, Map headers, - RequestResult requestResult) { + AbstractHttpClientTest.RequestResult requestResult) { // Must be implemented if testAsync is true throw new UnsupportedOperationException() } - static int getPort(URI uri) { - if (uri.port != -1) { - return uri.port - } else if (uri.scheme == "http") { - return 80 - } else if (uri.scheme == "https") { - 443 - } else { - throw new IllegalArgumentException("Unexpected uri: $uri") + @Shared + def junitTest = new AbstractHttpClientTest() { + @Override + protected buildRequest(String method, URI uri, Map headers) { + return HttpClientTest.this.buildRequest(method, uri, headers) } - } - Integer responseCodeOnRedirectError() { - return null - } + @Override + protected int sendRequest(def request, String method, URI uri, Map headers) { + return HttpClientTest.this.sendRequest(request, method, uri, headers) + } - String userAgent() { - return null - } + @Override + protected void sendRequestWithCallback(def request, String method, URI uri, Map headers, + AbstractHttpClientTest.RequestResult requestResult) { + HttpClientTest.this.sendRequestWithCallback(request, method, uri, headers, requestResult) + } - /** A list of additional HTTP client span attributes extracted by the instrumentation per URI. */ - Set> httpAttributes(URI uri) { - [ - SemanticAttributes.HTTP_URL, - SemanticAttributes.HTTP_METHOD, - SemanticAttributes.HTTP_FLAVOR, - SemanticAttributes.HTTP_USER_AGENT - ] - } + @Override + protected String expectedClientSpanName(URI uri, String method) { + return HttpClientTest.this.expectedClientSpanName(uri, method) + } - def "basic #method request #url"() { - when: - def responseCode = doRequest(method, url) - - then: - responseCode == 200 - assertTraces(1) { - trace(0, 2) { - clientSpan(it, 0, null, method, url) - serverSpan(it, 1, span(0)) - } + @Override + protected Integer responseCodeOnRedirectError() { + return HttpClientTest.this.responseCodeOnRedirectError() } - where: - path << ["/success", "/success?with=params"] + @Override + protected String userAgent() { + return HttpClientTest.this.userAgent() + } - method = "GET" - url = resolveAddress(path) - } + @Override + protected Throwable clientSpanError(URI uri, Throwable exception) { + return HttpClientTest.this.clientSpanError(uri, exception) + } - def "basic #method request with parent"() { - when: - def uri = resolveAddress("/success") - def responseCode = runWithSpan("parent") { - doRequest(method, uri) + @Override + protected Set> httpAttributes(URI uri) { + return HttpClientTest.this.httpAttributes(uri) } - then: - responseCode == 200 - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - } - clientSpan(it, 1, span(0), method) - serverSpan(it, 2, span(1)) - } + @Override + protected SingleConnection createSingleConnection(String host, int port) { + return HttpClientTest.this.createSingleConnection(host, port) } - where: - method << BODY_METHODS - } + @Override + protected boolean testWithClientParent() { + return HttpClientTest.this.testWithClientParent() + } - def "should suppress nested CLIENT span if already under parent CLIENT span (#method)"() { - given: - assumeTrue(testWithClientParent()) + @Override + protected boolean testRedirects() { + return HttpClientTest.this.testRedirects() + } - when: - def uri = resolveAddress("/success") - def responseCode = runUnderParentClientSpan { - doRequest(method, uri) + @Override + protected boolean testCircularRedirects() { + return HttpClientTest.this.testCircularRedirects() } - then: - responseCode == 200 - // there should be 2 separate traces since the nested CLIENT span is suppressed - // (and the span context propagation along with it) - assertTraces(2) { - traces.sort(orderByRootSpanKind(CLIENT, SERVER)) - - trace(0, 1) { - span(0) { - name "parent-client-span" - kind CLIENT - hasNoParent() - } - } - trace(1, 1) { - serverSpan(it, 0) - } + // maximum number of redirects that http client follows before giving up + @Override + protected int maxRedirects() { + return HttpClientTest.this.maxRedirects() } - where: - method << BODY_METHODS - } + @Override + protected boolean testReusedRequest() { + return HttpClientTest.this.testReusedRequest() + } + @Override + protected boolean testConnectionFailure() { + return HttpClientTest.this.testConnectionFailure() + } - //FIXME: add tests for POST with large/chunked data + @Override + protected boolean testRemoteConnection() { + return HttpClientTest.this.testRemoteConnection() + } - def "trace request with callback and parent"() { - given: - assumeTrue(testCallback()) - assumeTrue(testCallbackWithParent()) + @Override + protected boolean testReadTimeout() { + return HttpClientTest.this.testReadTimeout() + } - when: - def uri = resolveAddress("/success") - def requestResult = runWithSpan("parent") { - doRequestWithCallback(method, uri) { - runWithSpan("child") {} - } + @Override + protected boolean testHttps() { + return HttpClientTest.this.testHttps() } - then: - requestResult.get() == 200 - // only one trace (client). - assertTraces(1) { - trace(0, 4) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - } - clientSpan(it, 1, span(0), method) - serverSpan(it, 2, span(1)) - span(3) { - name "child" - kind SpanKind.INTERNAL - childOf span(0) - } - } + @Override + protected boolean testCausality() { + return HttpClientTest.this.testCausality() } - where: - method = "GET" - } + @Override + protected boolean testCausalityWithCallback() { + return HttpClientTest.this.testCausalityWithCallback() + } - def "trace request with callback and no parent"() { - given: - assumeTrue(testCallback()) + @Override + protected boolean testCallback() { + return HttpClientTest.this.testCallback() + } - when: - def uri = resolveAddress("/success") - def requestResult = doRequestWithCallback(method, uri) { - runWithSpan("callback") { - } + @Override + protected boolean testCallbackWithParent() { + // FIXME: this hack is here because callback with parent is broken in play-ws when the stream() + // function is used. There is no way to stop a test from a derived class hence the flag + return HttpClientTest.this.testCallbackWithParent() } - then: - requestResult.get() == 200 - // only one trace (client). - assertTraces(2) { - trace(0, 2) { - clientSpan(it, 0, null, method) - serverSpan(it, 1, span(0)) - } - trace(1, 1) { - span(0) { - name "callback" - kind SpanKind.INTERNAL - hasNoParent() - } - } + @Override + protected boolean testErrorWithCallback() { + return HttpClientTest.this.testErrorWithCallback() } + } - where: - method = "GET" + @Shared + HttpClientTestServer server + + def setupSpec() { + server = new HttpClientTestServer(openTelemetry) + server.start() + junitTest.setTesting(testRunner(), server) } - def "basic #method request with 1 redirect"() { - // TODO quite a few clients create an extra span for the redirect - // This test should handle both types or we should unify how the clients work + def cleanupSpec() { + server.stop() + } - given: - assumeTrue(testRedirects()) - def uri = resolveAddress("/redirect") - - when: - def responseCode = doRequest(method, uri) - - then: - responseCode == 200 - assertTraces(1) { - trace(0, 3) { - clientSpan(it, 0, null, method, uri) - serverSpan(it, 1, span(0)) - serverSpan(it, 2, span(0)) - } + static int getPort(URI uri) { + if (uri.port != -1) { + return uri.port + } else if (uri.scheme == "http") { + return 80 + } else if (uri.scheme == "https") { + 443 + } else { + throw new IllegalArgumentException("Unexpected uri: $uri") } + } + + def "basic GET request #path"() { + expect: + junitTest.successfulGetRequest(path) where: - method = "GET" + path << ["/success", "/success?with=params"] } - def "basic #method request with 2 redirects"() { - given: - assumeTrue(testRedirects()) - def uri = resolveAddress("/another-redirect") - - when: - def responseCode = doRequest(method, uri) - - then: - responseCode == 200 - assertTraces(1) { - trace(0, 4) { - clientSpan(it, 0, null, method, uri) - serverSpan(it, 1, span(0)) - serverSpan(it, 2, span(0)) - serverSpan(it, 3, span(0)) - } - } + def "basic #method request with parent"() { + expect: + junitTest.successfulRequestWithParent(method) where: - method = "GET" + method << BODY_METHODS } - def "basic #method request with circular redirects"() { - given: - assumeTrue(testRedirects() && testCircularRedirects()) - def uri = resolveAddress("/circular-redirect") - - when: - doRequest(method, uri) - - then: - def ex = thrown(Exception) - def thrownException = ex instanceof ExecutionException ? ex.cause : ex - - and: - assertTraces(1) { - trace(0, 1 + maxRedirects()) { - clientSpan(it, 0, null, method, uri, responseCodeOnRedirectError(), thrownException) - for (int i = 1; i < maxRedirects() + 1; i++) { - serverSpan(it, i, span(0)) - } - } - } + def "should suppress nested CLIENT span if already under parent CLIENT span (#method)"() { + assumeTrue(testWithClientParent()) + expect: + junitTest.shouldSuppressNestedClientSpanIfAlreadyUnderParentClientSpan(method) where: - method = "GET" + method << BODY_METHODS } - def "redirect #method to secured endpoint copies auth header"() { - given: - assumeTrue(testRedirects()) - def uri = resolveAddress("/to-secured") - when: + //FIXME: add tests for POST with large/chunked data - def responseCode = doRequest(method, uri, [(BASIC_AUTH_KEY): BASIC_AUTH_VAL]) + def "trace request with callback and parent"() { + assumeTrue(testCallback()) + assumeTrue(testCallbackWithParent()) + expect: + junitTest.requestWithCallbackAndParent() + } - then: - responseCode == 200 - assertTraces(1) { - trace(0, 3) { - clientSpan(it, 0, null, method, uri) - serverSpan(it, 1, span(0)) - serverSpan(it, 2, span(0)) - } - } + def "trace request with callback and no parent"() { + assumeTrue(testCallback()) + expect: + junitTest.requestWithCallbackAndNoParent() + } - where: - method = "GET" + def "basic request with 1 redirect"() { + assumeTrue(testRedirects()) + expect: + junitTest.basicRequestWith1Redirect() } - def "error span"() { - def uri = resolveAddress("/error") - when: - runWithSpan("parent") { - try { - doRequest(method, uri) - } catch (Exception ignored) { - } - } + def "basic request with 2 redirects"() { + assumeTrue(testRedirects()) + expect: + junitTest.basicRequestWith2Redirects() + } - then: - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - } - clientSpan(it, 1, span(0), method, uri, 500) - serverSpan(it, 2, span(1)) - } - } + def "basic request with circular redirects"() { + assumeTrue(testRedirects()) + assumeTrue(testCircularRedirects()) + expect: + junitTest.circularRedirects() + } - where: - method = "GET" + def "redirect to secured endpoint copies auth header"() { + assumeTrue(testRedirects()) + expect: + junitTest.redirectToSecuredCopiesAuthHeader() + } + + def "error span"() { + expect: + junitTest.errorSpan() } def "reuse request"() { - given: assumeTrue(testReusedRequest()) - - when: - def responseCode = doReusedRequest(method, url) - - then: - responseCode == 200 - assertTraces(2) { - trace(0, 2) { - clientSpan(it, 0, null, method, url) - serverSpan(it, 1, span(0)) - } - trace(1, 2) { - clientSpan(it, 0, null, method, url) - serverSpan(it, 1, span(0)) - } - } - - where: - path = "/success" - method = "GET" - url = resolveAddress(path) + expect: + junitTest.reuseRequest() } // this test verifies two things: @@ -630,180 +330,43 @@ abstract class HttpClientTest extends InstrumentationSpecification { // (so that it propagates the same trace id / span id that it reports to the backend // and the trace is not broken) def "request with existing tracing headers"() { - when: - def responseCode = doRequestWithExistingTracingHeaders(method, url) - - then: - responseCode == 200 - assertTraces(1) { - trace(0, 2) { - clientSpan(it, 0, null, method, url) - serverSpan(it, 1, span(0)) - } - } - - where: - path = "/success" - method = "GET" - url = resolveAddress(path) + expect: + junitTest.requestWithExistingTracingHeaders() } def "connection error (unopened port)"() { - given: assumeTrue(testConnectionFailure()) - def uri = new URI("http://localhost:$UNUSABLE_PORT/") - - when: - runWithSpan("parent") { - doRequest(method, uri) - } - - then: - def ex = thrown(Exception) - def thrownException = ex instanceof ExecutionException ? ex.cause : ex - - and: - assertTraces(1) { - trace(0, 2) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - status ERROR - errorEvent(thrownException.class, thrownException.message) - } - clientSpan(it, 1, span(0), method, uri, null, thrownException) - } - } - - where: - method = "GET" + expect: + junitTest.connectionErrorUnopenedPort() } def "connection error (unopened port) with callback"() { - given: assumeTrue(testConnectionFailure()) assumeTrue(testCallback()) assumeTrue(testErrorWithCallback()) - def uri = new URI("http://localhost:$UNUSABLE_PORT/") - - when: - def requestResult = runWithSpan("parent") { - doRequestWithCallback(method, uri, [:]) { - runWithSpan("callback") { - } - } - } - requestResult.get() - - then: - def ex = thrown(Exception) - def thrownException = ex instanceof ExecutionException ? ex.cause : ex - - and: - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - } - clientSpan(it, 1, span(0), method, uri, null, thrownException) - span(2) { - name "callback" - kind SpanKind.INTERNAL - childOf span(0) - } - } - } - - where: - method = "GET" + expect: + junitTest.connectionErrorUnopenedPortWithCallback() } def "connection error non routable address"() { - given: assumeTrue(testRemoteConnection()) - def uri = new URI("https://192.0.2.1/") - - when: - runWithSpan("parent") { - doRequest(method, uri) - } - - then: - def ex = thrown(Exception) - def thrownException = ex instanceof ExecutionException ? ex.cause : ex - assertTraces(1) { - trace(0, 2) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - status ERROR - errorEvent(thrownException.class, thrownException.message) - } - clientSpan(it, 1, span(0), method, uri, null, thrownException) - } - } - - where: - method = "HEAD" + expect: + junitTest.connectionErrorNonRoutableAddress() } - def "read timeout"() { - given: + def "read timed out"() { assumeTrue(testReadTimeout()) - def uri = resolveAddress("/read-timeout") - - when: - runWithSpan("parent") { - doRequest(method, uri) - } - - then: - def ex = thrown(Exception) - def thrownException = ex instanceof ExecutionException ? ex.cause : ex - assertTraces(1) { - trace(0, 3) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - status ERROR - errorEvent(thrownException.class, thrownException.message) - } - clientSpan(it, 1, span(0), method, uri, null, thrownException) - serverSpan(it, 2, span(1)) - } - } - - where: - method = "GET" + expect: + junitTest.readTimedOut() } // IBM JVM has different protocol support for TLS @Requires({ !System.getProperty("java.vm.name").contains("IBM J9 VM") }) def "test https request"() { - given: assumeTrue(testRemoteConnection()) assumeTrue(testHttps()) - def uri = new URI("https://localhost:${server.httpsPort()}/success") - - when: - def responseCode = doRequest(method, uri) - - then: - responseCode == 200 - assertTraces(1) { - trace(0, 2) { - clientSpan(it, 0, null, method, uri) - serverSpan(it, 1, span(0)) - } - } - - where: - method = "GET" + expect: + junitTest.httpsRequest() } /** @@ -814,112 +377,18 @@ abstract class HttpClientTest extends InstrumentationSpecification { * propagate trace context. */ def "high concurrency test"() { - setup: assumeTrue(testCausality()) - int count = 50 - def method = 'GET' - def url = resolveAddress("/success") - - def latch = new CountDownLatch(1) - - def pool = Executors.newFixedThreadPool(4) - - when: - count.times { index -> - def job = { - latch.await() - runWithSpan("Parent span " + index) { - Span.current().setAttribute("test.request.id", index) - doRequest(method, url, ["test-request-id": index.toString()]) - } - } - pool.submit(job) - } - latch.countDown() - - then: - assertTraces(count) { - count.times { idx -> - trace(idx, 3) { - def rootSpan = it.span(0) - //Traces can be in arbitrary order, let us find out the request id of the current one - def requestId = Integer.parseInt(rootSpan.name.substring("Parent span ".length())) - - span(0) { - name "Parent span " + requestId - kind SpanKind.INTERNAL - hasNoParent() - attributes { - "test.request.id" requestId - } - } - clientSpan(it, 1, span(0), method, url) - serverSpan(it, 2, span(1)) { - it."test.request.id" requestId - } - } - } - } + expect: + junitTest.highConcurrency() } def "high concurrency test with callback"() { - setup: assumeTrue(testCausality()) assumeTrue(testCausalityWithCallback()) assumeTrue(testCallback()) assumeTrue(testCallbackWithParent()) - - int count = 50 - def method = 'GET' - def url = resolveAddress("/success") - - def latch = new CountDownLatch(1) - - def pool = Executors.newFixedThreadPool(4) - - when: - count.times { index -> - def job = { - latch.await() - runWithSpan("Parent span " + index) { - Span.current().setAttribute("test.request.id", index) - doRequestWithCallback(method, url, ["test-request-id": index.toString()]) { - runWithSpan("child") {} - } - } - } - pool.submit(job) - } - latch.countDown() - - then: - assertTraces(count) { - count.times { idx -> - trace(idx, 4) { - def rootSpan = it.span(0) - //Traces can be in arbitrary order, let us find out the request id of the current one - def requestId = Integer.parseInt(rootSpan.name.substring("Parent span ".length())) - - span(0) { - name "Parent span " + requestId - kind SpanKind.INTERNAL - hasNoParent() - attributes { - "test.request.id" requestId - } - } - clientSpan(it, 1, span(0), method, url) - serverSpan(it, 2, span(1)) { - it."test.request.id" requestId - } - span(3) { - name "child" - kind SpanKind.INTERNAL - childOf span(0) - } - } - } - } + expect: + junitTest.highConcurrencyWithCallback() } /** @@ -927,157 +396,44 @@ abstract class HttpClientTest extends InstrumentationSpecification { * connection. */ def "high concurrency test on single connection"() { - setup: - def singleConnection = createSingleConnection("localhost", server.httpPort()) + SingleConnection singleConnection = createSingleConnection("localhost", server.httpPort()) assumeTrue(singleConnection != null) - int count = 50 - def method = 'GET' - def path = "/success" - def url = resolveAddress(path) - - def latch = new CountDownLatch(1) - def pool = Executors.newFixedThreadPool(4) - - when: - count.times { index -> - def job = { - latch.await() - runWithSpan("Parent span " + index) { - Span.current().setAttribute("test.request.id", index) - singleConnection.doRequest(path, [(SingleConnection.REQUEST_ID_HEADER): index.toString()]) - } - } - pool.submit(job) - } - latch.countDown() - - then: - assertTraces(count) { - count.times { idx -> - trace(idx, 3) { - def rootSpan = it.span(0) - //Traces can be in arbitrary order, let us find out the request id of the current one - def requestId = Integer.parseInt(rootSpan.name.substring("Parent span ".length())) - - span(0) { - name "Parent span " + requestId - kind SpanKind.INTERNAL - hasNoParent() - attributes { - "test.request.id" requestId - } - } - clientSpan(it, 1, span(0), method, url) - serverSpan(it, 2, span(1)) { - it."test.request.id" requestId - } - } - } - } + expect: + junitTest.highConcurrencyOnSingleConnection() } - //This method should create either a single connection to the target uri or a http client - //which is guaranteed to use the same connection for all requests - SingleConnection createSingleConnection(String host, int port) { - return null + // ideally private, but then groovy closures in this class cannot find them + final int doRequest(String method, URI uri, Map headers = [:]) { + def request = buildRequest(method, uri, headers) + return sendRequest(request, method, uri, headers) } - // parent span must be cast otherwise it breaks debugging classloading (junit loads it early) - void clientSpan(TraceAssert trace, int index, Object parentSpan, String method = "GET", URI uri = resolveAddress("/success"), Integer responseCode = 200, Throwable exception = null, String httpFlavor = "1.1") { - def userAgent = userAgent() - def httpClientAttributes = httpAttributes(uri) - trace.span(index) { - if (parentSpan == null) { - hasNoParent() - } else { - childOf((SpanData) parentSpan) - } - name expectedClientSpanName(uri, method) - kind CLIENT - if (exception) { - status ERROR - assertClientSpanErrorEvent(it, uri, exception) - } else if (responseCode >= 400) { - status ERROR - } - attributes { - "${SemanticAttributes.NET_TRANSPORT.key}" IP_TCP - if (uri.port == UNUSABLE_PORT || uri.host == "192.0.2.1") { - // TODO(anuraaga): For theses cases, there isn't actually a peer so we shouldn't be - // filling in peer information but some instrumentation does so based on the URL itself - // which is present in HTTP attributes. We should fix this. - "${SemanticAttributes.NET_PEER_NAME.key}" { it == null || it == uri.host } - "${SemanticAttributes.NET_PEER_PORT.key}" { it == null || it == uri.port || (uri.scheme == "https" && it == 443) } - } else { - "${SemanticAttributes.NET_PEER_NAME.key}" uri.host - "${SemanticAttributes.NET_PEER_PORT.key}" uri.port > 0 ? uri.port : { it == null || it == 443 } - } - "${SemanticAttributes.NET_PEER_IP.key}" { it == null || it == "127.0.0.1" || it == uri.host } // Optional - - if (httpClientAttributes.contains(SemanticAttributes.HTTP_URL)) { - "${SemanticAttributes.HTTP_URL.key}" { it == "${uri}" || it == "${removeFragment(uri)}" } - } - if (httpClientAttributes.contains(SemanticAttributes.HTTP_METHOD)) { - "${SemanticAttributes.HTTP_METHOD.key}" method - } - if (httpClientAttributes.contains(SemanticAttributes.HTTP_FLAVOR)) { - "${SemanticAttributes.HTTP_FLAVOR.key}" httpFlavor - } - if (httpClientAttributes.contains(SemanticAttributes.HTTP_USER_AGENT)) { - if (userAgent) { - "${SemanticAttributes.HTTP_USER_AGENT.key}" { it.startsWith(userAgent) } - } - } - if (httpClientAttributes.contains(SemanticAttributes.HTTP_HOST)) { - "${SemanticAttributes.HTTP_HOST}" { it == uri.host || it == "${uri.host}:${uri.port}" } - } - if (httpClientAttributes.contains(SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH)) { - "${SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH}" Long - } - if (httpClientAttributes.contains(SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH)) { - "${SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH}" Long - } - if (httpClientAttributes.contains(SemanticAttributes.HTTP_SCHEME)) { - "${SemanticAttributes.HTTP_SCHEME}" uri.scheme - } - if (httpClientAttributes.contains(SemanticAttributes.HTTP_TARGET)) { - "${SemanticAttributes.HTTP_TARGET}" uri.path + "${uri.query != null ? "?${uri.query}" : ""}" - } - - if (responseCode) { - "${SemanticAttributes.HTTP_STATUS_CODE.key}" responseCode - } - } - } + protected String expectedClientSpanName(URI uri, String method) { + return method != null ? "HTTP " + method : "HTTP request" } - void serverSpan(TraceAssert traces, int index, Object parentSpan = null, - @ClosureParams(value = SimpleType, options = ['io.opentelemetry.instrumentation.test.asserts.AttributesAssert']) - @DelegatesTo(value = AttributesAssert, strategy = Closure.DELEGATE_FIRST) Closure additionAttributesAssert = null) { - traces.span(index) { - name "test-http-server" - kind SERVER - if (parentSpan == null) { - hasNoParent() - } else { - childOf((SpanData) parentSpan) - } - if (additionAttributesAssert != null) { - attributes(additionAttributesAssert) - } - } + Integer responseCodeOnRedirectError() { + return null } - String expectedClientSpanName(URI uri, String method) { - return method != null ? "HTTP $method" : "HTTP request" + String userAgent() { + return null } - void assertClientSpanErrorEvent(SpanAssert spanAssert, URI uri, Throwable exception) { - assertClientSpanErrorEvent(spanAssert, uri, exception.class, exception.message) + /** A list of additional HTTP client span attributes extracted by the instrumentation per URI. */ + Set> httpAttributes(URI uri) { + [ + SemanticAttributes.HTTP_URL, + SemanticAttributes.HTTP_METHOD, + SemanticAttributes.HTTP_FLAVOR, + SemanticAttributes.HTTP_USER_AGENT + ] } - void assertClientSpanErrorEvent(SpanAssert spanAssert, URI uri, Class errorType, message) { - spanAssert.errorEvent(errorType, message) + //This method should create either a single connection to the target uri or a http client + //which is guaranteed to use the same connection for all requests + SingleConnection createSingleConnection(String host, int port) { + return null } boolean testWithClientParent() { @@ -1105,14 +461,14 @@ abstract class HttpClientTest extends InstrumentationSpecification { true } - boolean testReadTimeout() { - false - } - boolean testRemoteConnection() { true } + boolean testReadTimeout() { + false + } + boolean testHttps() { true } @@ -1139,11 +495,33 @@ abstract class HttpClientTest extends InstrumentationSpecification { return true } - URI removeFragment(URI uri) { - return new URI(uri.scheme, null, uri.host, uri.port, uri.path, uri.query, null) + Throwable clientSpanError(URI uri, Throwable exception) { + return exception + } + + final void clientSpan(TraceAssert trace, int index, Object parentSpan, String method = "GET", URI uri = resolveAddress("/success"), Integer responseCode = 200) { + trace.assertedIndexes.add(index) + def spanData = trace.span(index) + def assertion = junitTest.assertClientSpan(OpenTelemetryAssertions.assertThat(spanData), uri, method, responseCode) + if (parentSpan == null) { + assertion.hasParentSpanId(SpanId.invalid) + } else { + assertion.hasParentSpanId(((SpanData) parentSpan).spanId) + } + } + + final void serverSpan(TraceAssert trace, int index, Object parentSpan = null) { + trace.assertedIndexes.add(index) + def spanData = trace.span(index) + def assertion = junitTest.assertServerSpan(OpenTelemetryAssertions.assertThat(spanData)) + if (parentSpan == null) { + assertion.hasParentSpanId(SpanId.invalid) + } else { + assertion.hasParentSpanId(((SpanData) parentSpan).spanId) + } } - protected URI resolveAddress(String path) { - return URI.create("http://localhost:${server.httpPort()}${path}") + final URI resolveAddress(String path) { + return junitTest.resolveAddress(path) } } diff --git a/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/utils/TraceUtils.groovy b/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/utils/TraceUtils.groovy index 2d4e697f4d8d..42c60a4142af 100644 --- a/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/utils/TraceUtils.groovy +++ b/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/utils/TraceUtils.groovy @@ -5,13 +5,11 @@ package io.opentelemetry.instrumentation.test.utils - import io.opentelemetry.api.GlobalOpenTelemetry import io.opentelemetry.api.trace.Span import io.opentelemetry.api.trace.SpanKind import io.opentelemetry.api.trace.StatusCode import io.opentelemetry.api.trace.Tracer -import io.opentelemetry.extension.annotations.WithSpan import io.opentelemetry.instrumentation.test.server.ServerTraceUtils import io.opentelemetry.instrumentation.testing.util.ThrowingRunnable import java.util.concurrent.Callable @@ -57,13 +55,6 @@ class TraceUtils { tracer.spanBuilder(spanName).startSpan().end() } - // Must create span within agent using annotation until - // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/1726 - @WithSpan(value = "parent-client-span", kind = SpanKind.CLIENT) - static T runUnderParentClientSpan(Callable r) { - r.call() - } - static T runUnderTraceWithoutExceptionCatch(String spanName, Callable r) { Span span = tracer.spanBuilder(spanName).setSpanKind(SpanKind.INTERNAL).startSpan() diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/InstrumentationTestRunner.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/InstrumentationTestRunner.java index 4159d1f906c9..790c4cbaaefd 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/InstrumentationTestRunner.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/InstrumentationTestRunner.java @@ -5,12 +5,22 @@ package io.opentelemetry.instrumentation.testing; +import static org.awaitility.Awaitility.await; + import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil; import io.opentelemetry.instrumentation.testing.util.ThrowingRunnable; import io.opentelemetry.instrumentation.testing.util.ThrowingSupplier; import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.testing.assertj.TraceAssert; +import io.opentelemetry.sdk.testing.assertj.TracesAssert; import io.opentelemetry.sdk.trace.data.SpanData; import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; +import java.util.stream.StreamSupport; +import org.awaitility.core.ConditionTimeoutException; /** * This interface defines a common set of operations for interaction with OpenTelemetry SDK and @@ -34,6 +44,43 @@ public interface InstrumentationTestRunner { boolean forceFlushCalled(); + /** Return a list of all captured traces, where each trace is a sorted list of spans. */ + default List> traces() { + return TelemetryDataUtil.groupTraces(getExportedSpans()); + } + + default List> waitForTraces(int numberOfTraces) { + try { + return TelemetryDataUtil.waitForTraces( + this::getExportedSpans, numberOfTraces, 20, TimeUnit.SECONDS); + } catch (TimeoutException | InterruptedException e) { + throw new AssertionError("Error waiting for " + numberOfTraces + " traces", e); + } + } + + default void waitAndAssertTraces(Consumer... assertions) { + try { + await() + .untilAsserted( + () -> { + List> traces = waitForTraces(assertions.length); + TracesAssert.assertThat(traces).hasTracesSatisfyingExactly(assertions); + }); + } catch (ConditionTimeoutException e) { + // Don't throw this failure since the stack is the awaitility thread, causing confusion. + // Instead, just assert one more time on the test thread, which will fail with a better stack + // trace. + // TODO(anuraaga): There is probably a better way to do this. + List> traces = waitForTraces(assertions.length); + TracesAssert.assertThat(traces).hasTracesSatisfyingExactly(assertions); + } + } + + default void waitAndAssertTraces(Iterable> assertions) { + waitAndAssertTraces( + StreamSupport.stream(assertions.spliterator(), false).toArray(Consumer[]::new)); + } + /** * Runs the provided {@code callback} inside the scope of an INTERNAL span with name {@code * spanName}. diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/InstrumentationExtension.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/InstrumentationExtension.java index 29382f16282a..233c9d8a2a90 100644 --- a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/InstrumentationExtension.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/InstrumentationExtension.java @@ -11,14 +11,12 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.context.ContextStorage; import io.opentelemetry.instrumentation.testing.InstrumentationTestRunner; -import io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil; import io.opentelemetry.instrumentation.testing.util.ThrowingRunnable; import io.opentelemetry.instrumentation.testing.util.ThrowingSupplier; import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.testing.assertj.TraceAssert; import io.opentelemetry.sdk.trace.data.SpanData; import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import org.assertj.core.api.ListAssert; import org.junit.jupiter.api.extension.AfterAllCallback; @@ -38,7 +36,7 @@ protected InstrumentationExtension(InstrumentationTestRunner testRunner) { } @Override - public void beforeAll(ExtensionContext extensionContext) { + public void beforeAll(ExtensionContext extensionContext) throws Exception { testRunner.beforeTestClass(); } @@ -56,7 +54,7 @@ public void afterEach(ExtensionContext context) throws Exception { } @Override - public void afterAll(ExtensionContext extensionContext) { + public void afterAll(ExtensionContext extensionContext) throws Exception { testRunner.afterTestClass(); } @@ -70,11 +68,6 @@ public List spans() { return testRunner.getExportedSpans(); } - /** Return a list of all captured traces, where each trace is a sorted list of spans. */ - public List> traces() { - return TelemetryDataUtil.groupTraces(spans()); - } - /** Return a list of all captured metrics. */ public List metrics() { return testRunner.getExportedMetrics(); @@ -100,9 +93,8 @@ public void waitAndAssertMetrics( } /** - * Removes all captured telemetry data. After calling this method {@link #spans()}, {@link - * #traces()} and {@link #metrics()} will return empty lists until more telemetry data is - * captured. + * Removes all captured telemetry data. After calling this method {@link #spans()} and {@link + * #metrics()} will return empty lists until more telemetry data is captured. */ public void clearData() { testRunner.clearAllExportedData(); @@ -112,25 +104,14 @@ public void clearData() { * Wait until at least {@code numberOfTraces} traces are completed and return all captured traces. * Note that there may be more than {@code numberOfTraces} collected. By default this waits up to * 20 seconds, then times out. - * - * @throws TimeoutException when the operation times out - * @throws InterruptedException when the current thread is interrupted */ - public List> waitForTraces(int numberOfTraces) - throws TimeoutException, InterruptedException { - return waitForTraces(numberOfTraces, DEFAULT_TRACE_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); + public List> waitForTraces(int numberOfTraces) { + return testRunner.waitForTraces(numberOfTraces); } - /** - * Wait until at least {@code numberOfTraces} traces are completed and return all captured traces. - * Note that there may be more than {@code numberOfTraces} collected. - * - * @throws TimeoutException when the operation times out - * @throws InterruptedException when the current thread is interrupted - */ - public List> waitForTraces(int numberOfTraces, long timeout, TimeUnit unit) - throws TimeoutException, InterruptedException { - return TelemetryDataUtil.waitForTraces(this::spans, numberOfTraces, timeout, unit); + @SafeVarargs + public final void waitAndAssertTraces(Consumer... assertions) { + testRunner.waitAndAssertTraces(assertions); } /** @@ -186,4 +167,8 @@ public T runWithServerSpan( String spanName, ThrowingSupplier callback) throws E { return testRunner.runWithServerSpan(spanName, callback); } + + protected InstrumentationTestRunner getTestRunner() { + return testRunner; + } } 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 new file mode 100644 index 000000000000..5d985ca1de9c --- /dev/null +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/AbstractHttpClientTest.java @@ -0,0 +1,1218 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.testing.junit.http; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.extension.annotations.WithSpan; +import io.opentelemetry.instrumentation.test.utils.PortUtils; +import io.opentelemetry.instrumentation.testing.InstrumentationTestRunner; +import io.opentelemetry.sdk.testing.assertj.EventDataAssert; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.sdk.testing.assertj.TraceAssert; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.net.URI; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.IntStream; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfSystemProperty; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public abstract class AbstractHttpClientTest { + static final String BASIC_AUTH_KEY = "custom-authorization-header"; + static final String BASIC_AUTH_VAL = "plain text auth token"; + + /** + * Build the request to be passed to {@link #sendRequest(java.lang.Object, java.lang.String, + * java.net.URI, java.util.Map)}. + * + *

By splitting this step out separate from {@code sendRequest}, tests and re-execute the same + * request a second time to verify that the traceparent header is not added multiple times to the + * request, and that the last one wins. Tests will fail if the header shows multiple times. + */ + protected abstract REQUEST buildRequest(String method, URI uri, Map headers); + + /** + * Helper class for capturing result of asynchronous request and running a callback when result is + * received. + */ + public static class RequestResult { + private static final long timeout = 10_000; + private final CountDownLatch valueReady = new CountDownLatch(1); + private final Runnable callback; + private int status; + private Throwable throwable; + + public RequestResult(Runnable callback) { + this.callback = callback; + } + + public void complete(int status) { + complete(() -> status, null); + } + + public void complete(Throwable throwable) { + complete(null, throwable); + } + + public void complete(Supplier status, Throwable throwable) { + if (throwable != null) { + this.throwable = throwable; + } else { + this.status = status.get(); + } + callback.run(); + valueReady.countDown(); + } + + public int get() throws Throwable { + if (!valueReady.await(timeout, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("Timed out waiting for response in " + timeout + "ms"); + } + if (throwable != null) { + throw throwable; + } + return status; + } + } + + /** + * Make the request and return the status code of the response synchronously. Some clients, e.g., + * HTTPUrlConnection only support synchronous execution without callbacks, and many offer a + * dedicated API for invoking synchronously, such as OkHttp's execute method. + */ + protected abstract int sendRequest( + REQUEST request, String method, URI uri, Map headers); + + protected void sendRequestWithCallback( + REQUEST request, + String method, + URI uri, + Map headers, + RequestResult requestResult) { + // Must be implemented if testAsync is true + throw new UnsupportedOperationException(); + } + + /** Returns the connection timeout that should be used when setting up tested clients. */ + protected final Duration connectTimeout() { + return Duration.ofSeconds(5); + } + + protected final Duration readTimeout() { + return Duration.ofSeconds(2); + } + + private InstrumentationTestRunner testing; + private HttpClientTestServer server; + + @BeforeEach + void verifyExtension() { + if (testing == null) { + throw new AssertionError( + "Subclasses of AbstractHttpClientTest must register either " + + "HttpClientLibraryInstrumentationExtension or " + + "HttpClientAgentInstrumentationExtension"); + } + } + + @ParameterizedTest + @ValueSource(strings = {"/success", "/success?with=params"}) + void successfulGetRequest(String path) { + URI uri = resolveAddress(path); + String method = "GET"; + int responseCode = doRequest(method, uri); + + assertThat(responseCode).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> { + // Workaroud until release of + // https://github.com/open-telemetry/opentelemetry-java/pull/3386 + // in 1.5 + List> traces = testing.traces(); + trace.hasSpansSatisfyingExactly( + span -> + assertClientSpan(span, uri, method, responseCode) + .hasParentSpanId(SpanId.getInvalid()), + span -> assertServerSpan(span).hasParentSpanId(traces.get(0).get(0).getSpanId())); + }); + } + + @ParameterizedTest + @ValueSource(strings = {"PUT", "POST"}) + void successfulRequestWithParent(String method) { + URI uri = resolveAddress("/success"); + int responseCode = testing.runWithSpan("parent", () -> doRequest(method, uri)); + + assertThat(responseCode).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> { + // Workaroud until release of + // https://github.com/open-telemetry/opentelemetry-java/pull/3386 + // in 1.5 + List> traces = testing.traces(); + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()), + span -> + assertClientSpan(span, uri, method, responseCode) + .hasParentSpanId(traces.get(0).get(0).getSpanId()), + span -> assertServerSpan(span).hasParentSpanId(traces.get(0).get(1).getSpanId())); + }); + } + + @ParameterizedTest + @ValueSource(strings = {"PUT", "POST"}) + void shouldSuppressNestedClientSpanIfAlreadyUnderParentClientSpan(String method) { + assumeTrue(testWithClientParent()); + + URI uri = resolveAddress("/success"); + int responseCode = runUnderParentClientSpan(() -> doRequest(method, uri)); + + assertThat(responseCode).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent-client-span") + .hasKind(SpanKind.CLIENT) + .hasParentSpanId(SpanId.getInvalid())), + trace -> trace.hasSpansSatisfyingExactly(span -> assertServerSpan(span))); + } + + // FIXME: add tests for POST with large/chunked data + + @Test + void requestWithCallbackAndParent() throws Throwable { + assumeTrue(testCallback()); + assumeTrue(testCallbackWithParent()); + + String method = "GET"; + URI uri = resolveAddress("/success"); + + RequestResult result = + testing.runWithSpan( + "parent", + () -> doRequestWithCallback(method, uri, () -> testing.runWithSpan("child", () -> {}))); + + assertThat(result.get()).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> { + // Workaroud until release of + // https://github.com/open-telemetry/opentelemetry-java/pull/3386 + // in 1.5 + List> traces = testing.traces(); + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()), + span -> + assertClientSpan(span, uri, method, 200) + .hasParentSpanId(traces.get(0).get(0).getSpanId()), + span -> assertServerSpan(span).hasParentSpanId(traces.get(0).get(1).getSpanId()), + span -> + span.hasName("child") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(traces.get(0).get(0).getSpanId())); + }); + } + + @Test + void requestWithCallbackAndNoParent() throws Throwable { + assumeTrue(testCallback()); + + String method = "GET"; + URI uri = resolveAddress("/success"); + + RequestResult result = + doRequestWithCallback(method, uri, () -> testing.runWithSpan("callback", () -> {})); + + assertThat(result.get()).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> { + // Workaroud until release of + // https://github.com/open-telemetry/opentelemetry-java/pull/3386 + // in 1.5 + List> traces = testing.traces(); + trace.hasSpansSatisfyingExactly( + span -> assertClientSpan(span, uri, method, 200).hasParentSpanId(SpanId.getInvalid()), + span -> assertServerSpan(span).hasParentSpanId(traces.get(0).get(0).getSpanId())); + }, + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()))); + } + + @Test + void basicRequestWith1Redirect() { + // TODO quite a few clients create an extra span for the redirect + // This test should handle both types or we should unify how the clients work + + assumeTrue(testRedirects()); + + String method = "GET"; + URI uri = resolveAddress("/redirect"); + + int responseCode = doRequest(method, uri); + + assertThat(responseCode).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> { + // Workaroud until release of + // https://github.com/open-telemetry/opentelemetry-java/pull/3386 + // in 1.5 + List> traces = testing.traces(); + trace.hasSpansSatisfyingExactly( + span -> + assertClientSpan(span, uri, method, responseCode) + .hasParentSpanId(SpanId.getInvalid()), + span -> assertServerSpan(span).hasParentSpanId(traces.get(0).get(0).getSpanId()), + span -> assertServerSpan(span).hasParentSpanId(traces.get(0).get(0).getSpanId())); + }); + } + + @Test + void basicRequestWith2Redirects() { + // TODO quite a few clients create an extra span for the redirect + // This test should handle both types or we should unify how the clients work + + assumeTrue(testRedirects()); + + String method = "GET"; + URI uri = resolveAddress("/another-redirect"); + + int responseCode = doRequest(method, uri); + + assertThat(responseCode).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> { + // Workaroud until release of + // https://github.com/open-telemetry/opentelemetry-java/pull/3386 + // in 1.5 + List> traces = testing.traces(); + trace.hasSpansSatisfyingExactly( + span -> + assertClientSpan(span, uri, method, responseCode) + .hasParentSpanId(SpanId.getInvalid()), + span -> assertServerSpan(span).hasParentSpanId(traces.get(0).get(0).getSpanId()), + span -> assertServerSpan(span).hasParentSpanId(traces.get(0).get(0).getSpanId()), + span -> assertServerSpan(span).hasParentSpanId(traces.get(0).get(0).getSpanId())); + }); + } + + @Test + void circularRedirects() { + assumeTrue(testRedirects()); + assumeTrue(testCircularRedirects()); + + String method = "GET"; + URI uri = resolveAddress("/circular-redirect"); + + Throwable thrown = catchThrowable(() -> doRequest(method, uri)); + final Throwable ex; + if (thrown instanceof ExecutionException) { + ex = thrown.getCause(); + } else { + ex = thrown; + } + Throwable clientError = clientSpanError(uri, ex); + + testing.waitAndAssertTraces( + trace -> { + // Workaroud until release of + // https://github.com/open-telemetry/opentelemetry-java/pull/3386 + // in 1.5 + List> traces = testing.traces(); + List> assertions = new ArrayList<>(); + assertions.add( + span -> + assertClientSpan(span, uri, method, responseCodeOnRedirectError()) + .hasParentSpanId(SpanId.getInvalid()) + .hasEventsSatisfyingExactly(hasException(clientError))); + for (int i = 0; i < maxRedirects(); i++) { + assertions.add( + span -> assertServerSpan(span).hasParentSpanId(traces.get(0).get(0).getSpanId())); + } + trace.hasSpansSatisfyingExactly(assertions.toArray(new Consumer[0])); + }); + } + + @Test + void redirectToSecuredCopiesAuthHeader() { + assumeTrue(testRedirects()); + + String method = "GET"; + URI uri = resolveAddress("/to-secured"); + + int responseCode = + doRequest(method, uri, Collections.singletonMap(BASIC_AUTH_KEY, BASIC_AUTH_VAL)); + + assertThat(responseCode).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> { + // Workaroud until release of + // https://github.com/open-telemetry/opentelemetry-java/pull/3386 + // in 1.5 + List> traces = testing.traces(); + trace.hasSpansSatisfyingExactly( + span -> assertClientSpan(span, uri, method, 200).hasParentSpanId(SpanId.getInvalid()), + span -> assertServerSpan(span).hasParentSpanId(traces.get(0).get(0).getSpanId()), + span -> assertServerSpan(span).hasParentSpanId(traces.get(0).get(0).getSpanId())); + }); + } + + @Test + void errorSpan() { + String method = "GET"; + URI uri = resolveAddress("/error"); + + testing.runWithSpan( + "parent", + () -> { + try { + doRequest(method, uri); + } catch (Throwable ignored) { + } + }); + + testing.waitAndAssertTraces( + trace -> { + // Workaroud until release of + // https://github.com/open-telemetry/opentelemetry-java/pull/3386 + // in 1.5 + List> traces = testing.traces(); + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()), + span -> + assertClientSpan(span, uri, method, 500) + .hasParentSpanId(traces.get(0).get(0).getSpanId()), + span -> assertServerSpan(span).hasParentSpanId(traces.get(0).get(1).getSpanId())); + }); + } + + @Test + void reuseRequest() { + assumeTrue(testReusedRequest()); + + String method = "GET"; + URI uri = resolveAddress("/success"); + + int responseCode = doReusedRequest(method, uri); + + assertThat(responseCode).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> { + // Workaroud until release of + // https://github.com/open-telemetry/opentelemetry-java/pull/3386 + // in 1.5 + List> traces = testing.traces(); + trace.hasSpansSatisfyingExactly( + span -> + assertClientSpan(span, uri, method, responseCode) + .hasParentSpanId(SpanId.getInvalid()), + span -> assertServerSpan(span).hasParentSpanId(traces.get(0).get(0).getSpanId())); + }, + trace -> { + // Workaroud until release of + // https://github.com/open-telemetry/opentelemetry-java/pull/3386 + // in 1.5 + List> traces = testing.traces(); + trace.hasSpansSatisfyingExactly( + span -> + assertClientSpan(span, uri, method, responseCode) + .hasParentSpanId(SpanId.getInvalid()), + span -> assertServerSpan(span).hasParentSpanId(traces.get(1).get(0).getSpanId())); + }); + } + + // this test verifies two things: + // * the javaagent doesn't cause multiples of tracing headers to be added + // (TestHttpServer throws exception if there are multiples) + // * the javaagent overwrites the existing tracing headers + // (so that it propagates the same trace id / span id that it reports to the backend + // and the trace is not broken) + @Test + void requestWithExistingTracingHeaders() { + String method = "GET"; + URI uri = resolveAddress("/success"); + + int responseCode = doRequestWithExistingTracingHeaders(method, uri); + + assertThat(responseCode).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> { + // Workaroud until release of + // https://github.com/open-telemetry/opentelemetry-java/pull/3386 + // in 1.5 + List> traces = testing.traces(); + trace.hasSpansSatisfyingExactly( + span -> + assertClientSpan(span, uri, method, responseCode) + .hasParentSpanId(SpanId.getInvalid()), + span -> assertServerSpan(span).hasParentSpanId(traces.get(0).get(0).getSpanId())); + }); + } + + @Test + void connectionErrorUnopenedPort() { + assumeTrue(testConnectionFailure()); + + String method = "GET"; + URI uri = URI.create("http://localhost:" + PortUtils.UNUSABLE_PORT + '/'); + + Throwable thrown = + catchThrowable(() -> testing.runWithSpan("parent", () -> doRequest(method, uri))); + final Throwable ex; + if (thrown instanceof ExecutionException) { + ex = thrown.getCause(); + } else { + ex = thrown; + } + Throwable clientError = clientSpanError(uri, ex); + + testing.waitAndAssertTraces( + trace -> { + // Workaroud until release of + // https://github.com/open-telemetry/opentelemetry-java/pull/3386 + // in 1.5 + List> traces = testing.traces(); + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasStatus(StatusData.error()) + // Workaround until release of + // https://github.com/open-telemetry/opentelemetry-java/pull/3409 + // in 1.5 + .hasEventsSatisfyingExactly(hasException(ex)), + span -> + assertClientSpan(span, uri, method, null) + .hasParentSpanId(traces.get(0).get(0).getSpanId()) + .hasEventsSatisfyingExactly(hasException(clientError))); + }); + } + + @Test + void connectionErrorUnopenedPortWithCallback() { + assumeTrue(testConnectionFailure()); + assumeTrue(testCallback()); + assumeTrue(testErrorWithCallback()); + + String method = "GET"; + URI uri = URI.create("http://localhost:" + PortUtils.UNUSABLE_PORT + '/'); + + RequestResult result = + testing.runWithSpan( + "parent", + () -> + doRequestWithCallback( + method, uri, () -> testing.runWithSpan("callback", () -> {}))); + + Throwable thrown = catchThrowable(result::get); + final Throwable ex; + if (thrown instanceof ExecutionException) { + ex = thrown.getCause(); + } else { + ex = thrown; + } + Throwable clientError = clientSpanError(uri, ex); + + testing.waitAndAssertTraces( + trace -> { + // Workaroud until release of + // https://github.com/open-telemetry/opentelemetry-java/pull/3386 + // in 1.5 + List> traces = testing.traces(); + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()), + span -> + assertClientSpan(span, uri, method, null) + .hasParentSpanId(traces.get(0).get(0).getSpanId()) + .hasEventsSatisfyingExactly(hasException(clientError)), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(traces.get(0).get(0).getSpanId())); + }); + } + + @Test + void connectionErrorNonRoutableAddress() { + assumeTrue(testRemoteConnection()); + + String method = "HEAD"; + URI uri = URI.create("https://192.0.2.1/"); + + Throwable thrown = + catchThrowable(() -> testing.runWithSpan("parent", () -> doRequest(method, uri))); + final Throwable ex; + if (thrown instanceof ExecutionException) { + ex = thrown.getCause(); + } else { + ex = thrown; + } + Throwable clientError = clientSpanError(uri, ex); + + testing.waitAndAssertTraces( + trace -> { + // Workaroud until release of + // https://github.com/open-telemetry/opentelemetry-java/pull/3386 + // in 1.5 + List> traces = testing.traces(); + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasStatus(StatusData.error()) + .hasEventsSatisfyingExactly(hasException(ex)), + span -> + assertClientSpan(span, uri, method, null) + .hasParentSpanId(traces.get(0).get(0).getSpanId()) + .hasEventsSatisfyingExactly(hasException(clientError))); + }); + } + + @Test + void readTimedOut() { + assumeTrue(testReadTimeout()); + + String method = "GET"; + URI uri = resolveAddress("/read-timeout"); + + Throwable thrown = + catchThrowable(() -> testing.runWithSpan("parent", () -> doRequest(method, uri))); + final Throwable ex; + if (thrown instanceof ExecutionException) { + ex = thrown.getCause(); + } else { + ex = thrown; + } + Throwable clientError = clientSpanError(uri, ex); + + testing.waitAndAssertTraces( + trace -> { + // Workaroud until release of + // https://github.com/open-telemetry/opentelemetry-java/pull/3386 + // in 1.5 + List> traces = testing.traces(); + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("parent") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasStatus(StatusData.error()) + .hasEventsSatisfyingExactly(hasException(ex)), + span -> + assertClientSpan(span, uri, method, null) + .hasParentSpanId(traces.get(0).get(0).getSpanId()) + .hasEventsSatisfyingExactly(hasException(clientError)), + span -> assertServerSpan(span).hasParentSpanId(traces.get(0).get(1).getSpanId())); + }); + } + + @DisabledIfSystemProperty( + named = "java.vm.name", + matches = ".*IBM J9 VM.*", + disabledReason = "IBM JVM has different protocol support for TLS") + @Test + void httpsRequest() { + assumeTrue(testRemoteConnection()); + assumeTrue(testHttps()); + + String method = "GET"; + URI uri = URI.create("https://localhost:" + server.httpsPort() + "/success"); + + int responseCode = doRequest(method, uri); + + assertThat(responseCode).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> { + // Workaroud until release of + // https://github.com/open-telemetry/opentelemetry-java/pull/3386 + // in 1.5 + List> traces = testing.traces(); + trace.hasSpansSatisfyingExactly( + span -> + assertClientSpan(span, uri, method, responseCode) + .hasParentSpanId(SpanId.getInvalid()), + span -> assertServerSpan(span).hasParentSpanId(traces.get(0).get(0).getSpanId())); + }); + } + + /** + * This test fires a large number of concurrent requests. Each request first hits a HTTP server + * and then makes another client request. The goal of this test is to verify that in highly + * concurrent environment our instrumentations for http clients (especially inherently concurrent + * ones, such as Netty or Reactor) correctly propagate trace context. + */ + @Test + void highConcurrency() { + assumeTrue(testCausality()); + + int count = 50; + String method = "GET"; + URI uri = resolveAddress("/success"); + + CountDownLatch latch = new CountDownLatch(1); + + ExecutorService pool = Executors.newFixedThreadPool(4); + for (int i = 0; i < count; i++) { + int index = i; + Runnable job = + () -> { + try { + latch.await(); + } catch (InterruptedException e) { + throw new AssertionError(e); + } + testing.runWithSpan( + "Parent span " + index, + () -> { + Span.current().setAttribute("test.request.id", index); + doRequest( + method, + uri, + Collections.singletonMap("test-request-id", String.valueOf(index))); + }); + }; + pool.submit(job); + } + latch.countDown(); + + List> assertions = new ArrayList<>(); + for (int i = 0; i < count; i++) { + int idx = i; + assertions.add( + trace -> { + // Workaroud until release of + // https://github.com/open-telemetry/opentelemetry-java/pull/3386 + // in 1.5 + List> traces = testing.traces(); + SpanData rootSpan = traces.get(idx).get(0); + // Traces can be in arbitrary order, let us find out the request id of the current one + int requestId = Integer.parseInt(rootSpan.getName().substring("Parent span ".length())); + + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(rootSpan.getName()) + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attrs -> assertThat(attrs).containsEntry("test.request.id", requestId)), + span -> + assertClientSpan(span, uri, method, 200).hasParentSpanId(rootSpan.getSpanId()), + span -> + assertServerSpan(span) + .hasParentSpanId(traces.get(idx).get(1).getSpanId()) + .hasAttributesSatisfying( + attrs -> + assertThat(attrs).containsEntry("test.request.id", requestId))); + }); + } + + testing.waitAndAssertTraces(assertions); + + pool.shutdown(); + } + + @Test + void highConcurrencyWithCallback() { + assumeTrue(testCausality()); + assumeTrue(testCausalityWithCallback()); + assumeTrue(testCallback()); + assumeTrue(testCallbackWithParent()); + + int count = 50; + String method = "GET"; + URI uri = resolveAddress("/success"); + + CountDownLatch latch = new CountDownLatch(1); + + ExecutorService pool = Executors.newFixedThreadPool(4); + IntStream.range(0, count) + .forEach( + index -> { + Runnable job = + () -> { + try { + latch.await(); + } catch (InterruptedException e) { + throw new AssertionError(e); + } + testing.runWithSpan( + "Parent span " + index, + () -> { + Span.current().setAttribute("test.request.id", index); + doRequestWithCallback( + method, + uri, + Collections.singletonMap("test-request-id", String.valueOf(index)), + () -> testing.runWithSpan("child", () -> {})); + }); + }; + pool.submit(job); + }); + latch.countDown(); + + List> assertions = new ArrayList<>(); + for (int i = 0; i < count; i++) { + int idx = i; + assertions.add( + trace -> { + // Workaroud until release of + // https://github.com/open-telemetry/opentelemetry-java/pull/3386 + // in 1.5 + List> traces = testing.traces(); + SpanData rootSpan = traces.get(idx).get(0); + // Traces can be in arbitrary order, let us find out the request id of the current one + int requestId = Integer.parseInt(rootSpan.getName().substring("Parent span ".length())); + + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(rootSpan.getName()) + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attrs -> assertThat(attrs).containsEntry("test.request.id", requestId)), + span -> + assertClientSpan(span, uri, method, 200).hasParentSpanId(rootSpan.getSpanId()), + span -> + assertServerSpan(span) + .hasParentSpanId(traces.get(idx).get(1).getSpanId()) + .hasAttributesSatisfying( + attrs -> assertThat(attrs).containsEntry("test.request.id", requestId)), + span -> + span.hasName("child") + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(rootSpan.getSpanId())); + }); + } + + testing.waitAndAssertTraces(assertions); + + pool.shutdown(); + } + + /** + * Almost similar to the "high concurrency test" test above, but all requests use the same single + * connection. + */ + @Test + void highConcurrencyOnSingleConnection() { + SingleConnection singleConnection = createSingleConnection("localhost", server.httpPort()); + assumeTrue(singleConnection != null); + + int count = 50; + String method = "GET"; + String path = "/success"; + URI uri = resolveAddress(path); + + CountDownLatch latch = new CountDownLatch(1); + ExecutorService pool = Executors.newFixedThreadPool(4); + for (int i = 0; i < count; i++) { + int index = i; + Runnable job = + () -> { + try { + latch.await(); + } catch (InterruptedException e) { + throw new AssertionError(e); + } + testing.runWithSpan( + "Parent span " + index, + () -> { + Span.current().setAttribute("test.request.id", index); + try { + singleConnection.doRequest( + path, Collections.singletonMap("test-request-id", String.valueOf(index))); + } catch (InterruptedException e) { + throw new AssertionError(e); + } catch (Exception e) { + throw new AssertionError(e); + } + }); + }; + pool.submit(job); + } + latch.countDown(); + + List> assertions = new ArrayList<>(); + for (int i = 0; i < count; i++) { + int idx = i; + assertions.add( + trace -> { + // Workaroud until release of + // https://github.com/open-telemetry/opentelemetry-java/pull/3386 + // in 1.5 + List> traces = testing.traces(); + SpanData rootSpan = traces.get(idx).get(0); + // Traces can be in arbitrary order, let us find out the request id of the current one + int requestId = Integer.parseInt(rootSpan.getName().substring("Parent span ".length())); + + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(rootSpan.getName()) + .hasKind(SpanKind.INTERNAL) + .hasParentSpanId(SpanId.getInvalid()) + .hasAttributesSatisfying( + attrs -> assertThat(attrs).containsEntry("test.request.id", requestId)), + span -> + assertClientSpan(span, uri, method, 200).hasParentSpanId(rootSpan.getSpanId()), + span -> + assertServerSpan(span) + .hasParentSpanId(traces.get(idx).get(1).getSpanId()) + .hasAttributesSatisfying( + attrs -> + assertThat(attrs).containsEntry("test.request.id", requestId))); + }); + } + + testing.waitAndAssertTraces(assertions); + + pool.shutdown(); + } + + // Visible for spock bridge. + SpanDataAssert assertClientSpan( + SpanDataAssert span, URI uri, String method, Integer responseCode) { + Set> httpClientAttributes = httpAttributes(uri); + return span.hasName(expectedClientSpanName(uri, method)) + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfying( + attrs -> { + if (uri.getPort() == PortUtils.UNUSABLE_PORT || uri.getHost().equals("192.0.2.1")) { + // TODO(anuraaga): For theses cases, there isn't actually a peer so we shouldn't be + // filling in peer information but some instrumentation does so based on the URL + // itself which is present in HTTP attributes. We should fix this. + if (attrs.asMap().containsKey(SemanticAttributes.NET_PEER_NAME)) { + assertThat(attrs).containsEntry(SemanticAttributes.NET_PEER_NAME, uri.getHost()); + } + if (attrs.asMap().containsKey(SemanticAttributes.NET_PEER_PORT)) { + if (uri.getPort() > 0) { + assertThat(attrs) + .containsEntry(SemanticAttributes.NET_PEER_PORT, (long) uri.getPort()); + } else { + // https://192.0.2.1/ where some instrumentation may have set this to 443, but + // not all. + assertThat(attrs) + .hasEntrySatisfying( + SemanticAttributes.NET_PEER_PORT, + port -> { + // Some instrumentation seem to set NET_PEER_PORT to -1 incorrectly. + if (port > 0) { + assertThat(port).isEqualTo(443); + } + }); + } + } + } else { + assertThat(attrs).containsEntry(SemanticAttributes.NET_PEER_NAME, uri.getHost()); + // TODO(anuraaga): Remove cast after + // https://github.com/open-telemetry/opentelemetry-java/pull/3412 + assertThat(attrs) + .containsEntry(SemanticAttributes.NET_PEER_PORT, (long) uri.getPort()); + } + + // Optional + // TODO(anuraaga): Move to test knob rather than always treating + // as optional + if (attrs.asMap().containsKey(SemanticAttributes.NET_PEER_IP)) { + if (uri.getHost().equals("192.0.2.1")) { + // NB(anuraaga): This branch seems to currently only be exercised on Java 15. + // It would be good to understand how the JVM version is impacting this check. + assertThat(attrs).containsEntry(SemanticAttributes.NET_PEER_IP, "192.0.2.1"); + } else { + assertThat(attrs).containsEntry(SemanticAttributes.NET_PEER_IP, "127.0.0.1"); + } + } + + if (httpClientAttributes.contains(SemanticAttributes.HTTP_URL)) { + assertThat(attrs).containsEntry(SemanticAttributes.HTTP_URL, uri.toString()); + } + if (httpClientAttributes.contains(SemanticAttributes.HTTP_METHOD)) { + assertThat(attrs).containsEntry(SemanticAttributes.HTTP_METHOD, method); + } + if (httpClientAttributes.contains(SemanticAttributes.HTTP_FLAVOR)) { + // TODO(anuraaga): Support HTTP/2 + assertThat(attrs) + .containsEntry( + SemanticAttributes.HTTP_FLAVOR, + SemanticAttributes.HttpFlavorValues.HTTP_1_1); + } + if (httpClientAttributes.contains(SemanticAttributes.HTTP_USER_AGENT)) { + String userAgent = userAgent(); + if (userAgent != null) { + assertThat(attrs) + .hasEntrySatisfying( + SemanticAttributes.HTTP_USER_AGENT, + actual -> assertThat(actual).startsWith(userAgent)); + } + } + if (httpClientAttributes.contains(SemanticAttributes.HTTP_HOST)) { + // TODO(anuraaga): It's not well defined when instrumentation records with and + // without port. We should make this more uniform + assertThat(attrs) + .hasEntrySatisfying( + SemanticAttributes.HTTP_HOST, + host -> assertThat(host).startsWith(uri.getHost())); + } + if (httpClientAttributes.contains(SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH)) { + assertThat(attrs) + .hasEntrySatisfying( + SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, + length -> assertThat(length).isNotNegative()); + } + if (httpClientAttributes.contains(SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH)) { + assertThat(attrs) + .hasEntrySatisfying( + SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, + length -> assertThat(length).isNotNegative()); + } + if (httpClientAttributes.contains(SemanticAttributes.HTTP_SCHEME)) { + assertThat(attrs).containsEntry(SemanticAttributes.HTTP_SCHEME, uri.getScheme()); + } + if (httpClientAttributes.contains(SemanticAttributes.HTTP_TARGET)) { + String target = uri.getPath(); + if (uri.getQuery() != null) { + target += '?' + uri.getQuery(); + } + assertThat(attrs).containsEntry(SemanticAttributes.HTTP_TARGET, target); + } + + if (responseCode != null) { + assertThat(attrs) + .containsEntry(SemanticAttributes.HTTP_STATUS_CODE, (long) responseCode); + } + }); + } + + // Visible for spock bridge. + static SpanDataAssert assertServerSpan(SpanDataAssert span) { + return span.hasName("test-http-server").hasKind(SpanKind.SERVER); + } + + protected Set> httpAttributes(URI uri) { + Set> attributes = new HashSet<>(); + attributes.add(SemanticAttributes.HTTP_URL); + attributes.add(SemanticAttributes.HTTP_METHOD); + attributes.add(SemanticAttributes.HTTP_FLAVOR); + attributes.add(SemanticAttributes.HTTP_USER_AGENT); + return attributes; + } + + protected String expectedClientSpanName(URI uri, String method) { + return method != null ? "HTTP " + method : "HTTP request"; + } + + @Nullable + protected Integer responseCodeOnRedirectError() { + return null; + } + + @Nullable + protected String userAgent() { + return null; + } + + protected Throwable clientSpanError(URI uri, Throwable exception) { + return exception; + } + + // This method should create either a single connection to the target uri or a http client + // which is guaranteed to use the same connection for all requests + @Nullable + protected SingleConnection createSingleConnection(String host, int port) { + return null; + } + + protected boolean testWithClientParent() { + return true; + } + + protected boolean testRedirects() { + return true; + } + + protected boolean testCircularRedirects() { + return true; + } + + // maximum number of redirects that http client follows before giving up + protected int maxRedirects() { + return 2; + } + + protected boolean testReusedRequest() { + return true; + } + + protected boolean testConnectionFailure() { + return true; + } + + protected boolean testReadTimeout() { + return false; + } + + protected boolean testRemoteConnection() { + return true; + } + + protected boolean testHttps() { + return true; + } + + protected boolean testCausality() { + return true; + } + + protected boolean testCausalityWithCallback() { + return true; + } + + protected boolean testCallback() { + return true; + } + + protected boolean testCallbackWithParent() { + // FIXME: this hack is here because callback with parent is broken in play-ws when the stream() + // function is used. There is no way to stop a test from a derived class hence the flag + return true; + } + + protected boolean testErrorWithCallback() { + return true; + } + + // Workaround until release of + // https://github.com/open-telemetry/opentelemetry-java/pull/3409 + // in 1.5 + private static Consumer[] hasException(Throwable exception) { + return Collections.>singletonList( + event -> + event + .hasName(SemanticAttributes.EXCEPTION_EVENT_NAME) + .hasAttributesSatisfying( + attrs -> + assertThat(attrs) + .containsEntry( + SemanticAttributes.EXCEPTION_TYPE, + exception.getClass().getCanonicalName()) + .containsEntry( + SemanticAttributes.EXCEPTION_MESSAGE, exception.getMessage()))) + .toArray(new Consumer[0]); + } + + private int doRequest(String method, URI uri) { + return doRequest(method, uri, Collections.emptyMap()); + } + + private int doRequest(String method, URI uri, Map headers) { + REQUEST request = buildRequest(method, uri, headers); + return sendRequest(request, method, uri, headers); + } + + private int doReusedRequest(String method, URI uri) { + REQUEST request = buildRequest(method, uri, Collections.emptyMap()); + sendRequest(request, method, uri, Collections.emptyMap()); + return sendRequest(request, method, uri, Collections.emptyMap()); + } + + private int doRequestWithExistingTracingHeaders(String method, URI uri) { + Map headers = new HashMap(); + for (String field : + testing.getOpenTelemetry().getPropagators().getTextMapPropagator().fields()) { + headers.put(field, "12345789"); + } + REQUEST request = buildRequest(method, uri, headers); + return sendRequest(request, method, uri, headers); + } + + private RequestResult doRequestWithCallback(String method, URI uri, Runnable callback) { + return doRequestWithCallback(method, uri, Collections.emptyMap(), callback); + } + + private RequestResult doRequestWithCallback( + String method, URI uri, Map headers, Runnable callback) { + REQUEST request = buildRequest(method, uri, headers); + RequestResult requestResult = new RequestResult(callback); + sendRequestWithCallback(request, method, uri, headers, requestResult); + return requestResult; + } + + // Visible for spock bridge. + URI resolveAddress(String path) { + return URI.create("http://localhost:" + server.httpPort() + path); + } + + final void setTesting(InstrumentationTestRunner testing, HttpClientTestServer server) { + this.testing = testing; + this.server = server; + } + + // Must create span within agent using annotation until + // https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/1726 + @WithSpan(value = "parent-client-span", kind = SpanKind.CLIENT) + private static T runUnderParentClientSpan(Callable r) { + try { + return r.call(); + } catch (Throwable t) { + throw new AssertionError(t); + } + } +} diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientInstrumentationExtension.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientInstrumentationExtension.java new file mode 100644 index 000000000000..26d3acb70827 --- /dev/null +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientInstrumentationExtension.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.testing.junit.http; + +import io.opentelemetry.instrumentation.testing.AgentTestRunner; +import io.opentelemetry.instrumentation.testing.InstrumentationTestRunner; +import io.opentelemetry.instrumentation.testing.LibraryTestRunner; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.ExtensionContext; + +/** + * A {@link InstrumentationExtension} which sets up infrastructure, such as a test HTTP server, for + * {@link AbstractHttpClientTest}. + */ +public final class HttpClientInstrumentationExtension extends InstrumentationExtension { + + /** + * Returns a {@link InstrumentationExtension} to be used with {@link AbstractHttpClientTest} for + * javaagent instrumentation. + */ + public static InstrumentationExtension forAgent() { + return new HttpClientInstrumentationExtension(AgentTestRunner.instance()); + } + + /** + * Returns a {@link InstrumentationExtension} to be used with {@link AbstractHttpClientTest} for + * library instrumentation. + */ + public static InstrumentationExtension forLibrary() { + return new HttpClientInstrumentationExtension(LibraryTestRunner.instance()); + } + + private final HttpClientTestServer server; + + private HttpClientInstrumentationExtension(InstrumentationTestRunner runner) { + super(runner); + + server = new HttpClientTestServer(getOpenTelemetry()); + } + + @Override + public void beforeAll(ExtensionContext extensionContext) throws Exception { + super.beforeAll(extensionContext); + server.beforeAll(extensionContext); + } + + @Override + public void beforeEach(ExtensionContext extensionContext) { + super.beforeEach(extensionContext); + Object testInstance = extensionContext.getRequiredTestInstance(); + + if (!(testInstance instanceof AbstractHttpClientTest)) { + throw new AssertionError( + "HttpClientLibraryInstrumentationExtension can only be applied to a subclass of " + + "AbstractHttpClientTest"); + } + + ((AbstractHttpClientTest) testInstance).setTesting(getTestRunner(), server); + } + + @Override + public void afterAll(ExtensionContext extensionContext) throws Exception { + super.afterAll(extensionContext); + server.afterAll(extensionContext); + } +} diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientTestServer.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientTestServer.java new file mode 100644 index 000000000000..6b58fe5e66ce --- /dev/null +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientTestServer.java @@ -0,0 +1,122 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.testing.junit.http; + +import static io.opentelemetry.api.trace.SpanKind.SERVER; +import static io.opentelemetry.testing.internal.armeria.common.MediaType.PLAIN_TEXT_UTF_8; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.test.server.http.RequestContextGetter; +import io.opentelemetry.testing.internal.armeria.common.HttpData; +import io.opentelemetry.testing.internal.armeria.common.HttpResponse; +import io.opentelemetry.testing.internal.armeria.common.HttpStatus; +import io.opentelemetry.testing.internal.armeria.common.ResponseHeaders; +import io.opentelemetry.testing.internal.armeria.common.ResponseHeadersBuilder; +import io.opentelemetry.testing.internal.armeria.server.ServerBuilder; +import io.opentelemetry.testing.internal.armeria.server.logging.LoggingService; +import io.opentelemetry.testing.internal.armeria.testing.junit5.server.ServerExtension; +import java.io.FileInputStream; +import java.net.URI; +import java.security.KeyStore; +import java.time.Duration; +import javax.net.ssl.KeyManagerFactory; + +public final class HttpClientTestServer extends ServerExtension { + + private final OpenTelemetry openTelemetry; + private final Tracer tracer; + + public HttpClientTestServer(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + tracer = openTelemetry.getTracer("test"); + } + + @Override + protected void configure(ServerBuilder sb) throws Exception { + KeyStore keystore = KeyStore.getInstance("PKCS12"); + keystore.load( + new FileInputStream(System.getProperty("javax.net.ssl.trustStore")), + "testing".toCharArray()); + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keystore, "testing".toCharArray()); + + sb.http(0) + .https(0) + .tls(kmf) + .service( + "/success", + (ctx, req) -> { + ResponseHeadersBuilder headers = ResponseHeaders.builder(HttpStatus.OK); + String testRequestId = req.headers().get("test-request-id"); + if (testRequestId != null) { + headers.set("test-request-id", testRequestId); + } + return HttpResponse.of(headers.build(), HttpData.ofAscii("Hello.")); + }) + .service( + "/client-error", + (ctx, req) -> HttpResponse.of(HttpStatus.BAD_REQUEST, PLAIN_TEXT_UTF_8, "Invalid RQ")) + .service( + "/error", + (ctx, req) -> + HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, PLAIN_TEXT_UTF_8, "Sorry.")) + .service("/redirect", (ctx, req) -> HttpResponse.ofRedirect(HttpStatus.FOUND, "/success")) + .service( + "/another-redirect", + (ctx, req) -> HttpResponse.ofRedirect(HttpStatus.FOUND, "/redirect")) + .service( + "/circular-redirect", + (ctx, req) -> HttpResponse.ofRedirect(HttpStatus.FOUND, "/circular-redirect")) + .service( + "/secured", + (ctx, req) -> { + String auth = req.headers().get(AbstractHttpClientTest.BASIC_AUTH_KEY); + if (auth != null && auth.equals(AbstractHttpClientTest.BASIC_AUTH_VAL)) { + return HttpResponse.of( + HttpStatus.OK, PLAIN_TEXT_UTF_8, "secured string under basic auth"); + } + return HttpResponse.of(HttpStatus.UNAUTHORIZED, PLAIN_TEXT_UTF_8, "Unauthorized"); + }) + .service("/to-secured", (ctx, req) -> HttpResponse.ofRedirect(HttpStatus.FOUND, "/secured")) + .service( + "/read-timeout", + (ctx, req) -> + HttpResponse.delayed(HttpResponse.of(HttpStatus.OK), Duration.ofSeconds(20))) + .decorator( + (delegate, ctx, req) -> { + for (String field : openTelemetry.getPropagators().getTextMapPropagator().fields()) { + if (req.headers().getAll(field).size() > 1) { + throw new AssertionError((Object) ("more than one " + field + " header present")); + } + } + SpanBuilder span = + tracer + .spanBuilder("test-http-server") + .setSpanKind(SERVER) + .setParent( + openTelemetry + .getPropagators() + .getTextMapPropagator() + .extract(Context.current(), ctx, RequestContextGetter.INSTANCE)); + + String traceRequestId = req.headers().get("test-request-id"); + if (traceRequestId != null) { + span.setAttribute("test.request.id", Integer.parseInt(traceRequestId)); + } + span.startSpan().end(); + + return delegate.serve(ctx, req); + }) + .decorator(LoggingService.newDecorator()); + } + + URI resolveAddress(String path) { + return URI.create("http://localhost:" + httpPort() + path); + } +} diff --git a/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/SingleConnection.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/SingleConnection.java similarity index 93% rename from testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/SingleConnection.java rename to testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/SingleConnection.java index a074ced36a60..f28a79b89b97 100644 --- a/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/SingleConnection.java +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/SingleConnection.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.instrumentation.test.base; +package io.opentelemetry.instrumentation.testing.junit.http; import java.util.Map; import java.util.concurrent.ExecutionException;