
io.opentelemetry.instrumentation.test.base.HttpClientTest.groovy Maven / Gradle / Ivy
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.test.base
import static org.junit.jupiter.api.Assumptions.assumeTrue
import static org.junit.jupiter.api.Assumptions.assumeFalse
import io.opentelemetry.api.common.AttributeKey
import io.opentelemetry.api.trace.SpanId
import io.opentelemetry.instrumentation.test.InstrumentationSpecification
import io.opentelemetry.instrumentation.test.asserts.TraceAssert
import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions
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 spock.lang.Requires
import spock.lang.Shared
import spock.lang.Unroll
@Unroll
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
/**
* 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.
*/
abstract REQUEST buildRequest(String method, URI uri, Map headers)
/**
* 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. When implementing
* this method, such an API should be used and the HTTP status code of the response returned,
* for example:
*
*
* @Override
* int sendRequest(Request request, String method, URI uri, Map
*
* If there is no synchronous API available at all, for example as in Vert.X, a CompletableFuture
* can be used to block on a result, for example:
*
*
* @Override
* int sendRequest(Request request, String method, URI uri, Map headers) {
* CompletableFuture future = new CompletableFuture<>(
* sendRequestWithCallback(request, method, uri, headers) {
* future.complete(it.statusCode())
* }
* return future.get()
* }
*
*/
abstract int sendRequest(REQUEST request, String method, URI uri, Map headers)
/**
* Make the request and return the status code of the response through the callback. This method
* should be implemented if the client offers any request execution methods that accept a callback
* which receives the response. This will generally be an API for asynchronous execution of a
* request, such as OkHttp's enqueue method, but may also be a callback executed synchronously,
* such as ApacheHttpClient's response handler callbacks. This method is used in tests to verify
* the context is propagated correctly to such callbacks.
*
*
* @Override
* void sendRequestWithCallback(Request request, String method, URI uri, Map headers, RequestResult requestResult) {
* // Hypothetical client accepting a callback
* client.executeAsync(request) {
* void success(Response response) {
* requestResult.complete(response.statusCode())
* }
* void failure(Throwable throwable) {
* requestResult.complete(throwable)
* }
* }
*
* // Hypothetical client returning a CompletableFuture
* client.executeAsync(request).whenComplete { response, throwable ->
* requestResult.complete({ response.statusCode() }, throwable)
* }
* }
*
*
* If the client offers no APIs that accept callbacks, then this method should not be implemented
* and instead, {@link #testCallback} should be implemented to return false.
*/
void sendRequestWithCallback(REQUEST request, String method, URI uri, Map headers,
AbstractHttpClientTest.RequestResult requestResult) {
// Must be implemented if testAsync is true
throw new UnsupportedOperationException()
}
@Shared
def junitTest = new AbstractHttpClientTest() {
@Override
protected buildRequest(String method, URI uri, Map headers) {
return HttpClientTest.this.buildRequest(method, uri, headers)
}
@Override
protected int sendRequest(def request, String method, URI uri, Map headers) {
return HttpClientTest.this.sendRequest(request, method, uri, headers)
}
@Override
protected void sendRequestWithCallback(def request, String method, URI uri, Map headers,
AbstractHttpClientTest.RequestResult requestResult) {
HttpClientTest.this.sendRequestWithCallback(request, method, uri, headers, requestResult)
}
@Override
protected String expectedClientSpanName(URI uri, String method) {
return HttpClientTest.this.expectedClientSpanName(uri, method)
}
@Override
protected Integer responseCodeOnRedirectError() {
return HttpClientTest.this.responseCodeOnRedirectError()
}
@Override
protected String userAgent() {
return HttpClientTest.this.userAgent()
}
@Override
protected Throwable clientSpanError(URI uri, Throwable exception) {
return HttpClientTest.this.clientSpanError(uri, exception)
}
@Override
protected Set> httpAttributes(URI uri) {
return HttpClientTest.this.httpAttributes(uri)
}
@Override
protected SingleConnection createSingleConnection(String host, int port) {
return HttpClientTest.this.createSingleConnection(host, port)
}
@Override
protected boolean testWithClientParent() {
return HttpClientTest.this.testWithClientParent()
}
@Override
protected boolean testRedirects() {
return HttpClientTest.this.testRedirects()
}
@Override
protected boolean testCircularRedirects() {
return HttpClientTest.this.testCircularRedirects()
}
// maximum number of redirects that http client follows before giving up
@Override
protected int maxRedirects() {
return HttpClientTest.this.maxRedirects()
}
@Override
protected boolean testReusedRequest() {
return HttpClientTest.this.testReusedRequest()
}
@Override
protected boolean testConnectionFailure() {
return HttpClientTest.this.testConnectionFailure()
}
@Override
protected boolean testRemoteConnection() {
return HttpClientTest.this.testRemoteConnection()
}
@Override
protected boolean testReadTimeout() {
return HttpClientTest.this.testReadTimeout()
}
@Override
protected boolean testHttps() {
return HttpClientTest.this.testHttps()
}
@Override
protected boolean testCausality() {
return HttpClientTest.this.testCausality()
}
@Override
protected boolean testCausalityWithCallback() {
return HttpClientTest.this.testCausalityWithCallback()
}
@Override
protected boolean testCallback() {
return HttpClientTest.this.testCallback()
}
@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()
}
@Override
protected boolean testCallbackWithImplicitParent() {
return HttpClientTest.this.testCallbackWithImplicitParent()
}
@Override
protected boolean testErrorWithCallback() {
return HttpClientTest.this.testErrorWithCallback()
}
}
@Shared
HttpClientTestServer server
def setupSpec() {
server = new HttpClientTestServer(openTelemetry)
server.start()
junitTest.setupOptions()
junitTest.setTesting(testRunner(), server)
}
def cleanupSpec() {
server.stop()
}
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:
path << ["/success", "/success?with=params"]
}
def "basic #method request with parent"() {
expect:
junitTest.successfulRequestWithParent(method)
where:
method << BODY_METHODS
}
def "basic GET request with not sampled parent"() {
expect:
junitTest.successfulRequestWithNotSampledParent()
}
def "should suppress nested CLIENT span if already under parent CLIENT span (#method)"() {
assumeTrue(testWithClientParent())
expect:
junitTest.shouldSuppressNestedClientSpanIfAlreadyUnderParentClientSpan(method)
where:
method << BODY_METHODS
}
//FIXME: add tests for POST with large/chunked data
def "trace request with callback and parent"() {
assumeTrue(testCallback())
assumeTrue(testCallbackWithParent())
expect:
junitTest.requestWithCallbackAndParent()
}
def "trace request with callback and no parent"() {
assumeTrue(testCallback())
assumeFalse(testCallbackWithImplicitParent())
expect:
junitTest.requestWithCallbackAndNoParent()
}
def "trace request with callback and implicit parent"() {
assumeTrue(testCallback())
assumeTrue(testCallbackWithImplicitParent())
expect:
junitTest.requestWithCallbackAndImplicitParent()
}
def "basic request with 1 redirect"() {
assumeTrue(testRedirects())
expect:
junitTest.basicRequestWith1Redirect()
}
def "basic request with 2 redirects"() {
assumeTrue(testRedirects())
expect:
junitTest.basicRequestWith2Redirects()
}
def "basic request with circular redirects"() {
assumeTrue(testRedirects())
assumeTrue(testCircularRedirects())
expect:
junitTest.circularRedirects()
}
def "redirect to secured endpoint copies auth header"() {
assumeTrue(testRedirects())
expect:
junitTest.redirectToSecuredCopiesAuthHeader()
}
def "error span"() {
expect:
junitTest.errorSpan()
}
def "reuse request"() {
assumeTrue(testReusedRequest())
expect:
junitTest.reuseRequest()
}
// 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)
def "request with existing tracing headers"() {
expect:
junitTest.requestWithExistingTracingHeaders()
}
def "connection error (unopened port)"() {
assumeTrue(testConnectionFailure())
expect:
junitTest.connectionErrorUnopenedPort()
}
def "connection error (unopened port) with callback"() {
assumeTrue(testConnectionFailure())
assumeTrue(testCallback())
assumeTrue(testErrorWithCallback())
expect:
junitTest.connectionErrorUnopenedPortWithCallback()
}
def "connection error non routable address"() {
assumeTrue(testRemoteConnection())
expect:
junitTest.connectionErrorNonRoutableAddress()
}
def "read timed out"() {
assumeTrue(testReadTimeout())
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"() {
assumeTrue(testRemoteConnection())
assumeTrue(testHttps())
expect:
junitTest.httpsRequest()
}
/**
* 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.
*/
def "high concurrency test"() {
assumeTrue(testCausality())
expect:
junitTest.highConcurrency()
}
def "high concurrency test with callback"() {
assumeTrue(testCausality())
assumeTrue(testCausalityWithCallback())
assumeTrue(testCallback())
assumeTrue(testCallbackWithParent())
expect:
junitTest.highConcurrencyWithCallback()
}
/**
* Almost similar to the "high concurrency test" test above, but all requests use the same single
* connection.
*/
def "high concurrency test on single connection"() {
SingleConnection singleConnection = createSingleConnection("localhost", server.httpPort())
assumeTrue(singleConnection != null)
expect:
junitTest.highConcurrencyOnSingleConnection()
}
// 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)
}
protected String expectedClientSpanName(URI uri, String method) {
return method != null ? "HTTP " + method : "HTTP request"
}
Integer responseCodeOnRedirectError() {
return null
}
String userAgent() {
return null
}
/** A list of additional HTTP client span attributes extracted by the instrumentation per URI. */
Set> httpAttributes(URI uri) {
new HashSet<>(HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES)
}
//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() {
true
}
boolean testRedirects() {
true
}
boolean testCircularRedirects() {
true
}
// maximum number of redirects that http client follows before giving up
int maxRedirects() {
2
}
boolean testReusedRequest() {
true
}
boolean testConnectionFailure() {
true
}
boolean testRemoteConnection() {
true
}
boolean testReadTimeout() {
false
}
boolean testHttps() {
true
}
boolean testCausality() {
true
}
boolean testCausalityWithCallback() {
true
}
boolean testCallback() {
return true
}
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
true
}
boolean testCallbackWithImplicitParent() {
// depending on async behavior callback can be executed within
// parent span scope or outside of the scope, e.g. in reactor-netty or spring
// callback is correlated.
false
}
boolean testErrorWithCallback() {
return true
}
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)
}
}
final URI resolveAddress(String path) {
return junitTest.resolveAddress(path)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy