io.opentelemetry.instrumentation.test.base.HttpClientTest.groovy Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of opentelemetry-testing-common Show documentation
Show all versions of opentelemetry-testing-common Show documentation
OpenTelemetry Javaagent testing commons
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.test.base
import io.opentelemetry.api.common.AttributeKey
import io.opentelemetry.api.trace.SpanId
import io.opentelemetry.instrumentation.api.internal.HttpConstants
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.HttpClientResult
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
import static org.junit.jupiter.api.Assumptions.assumeTrue
@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, HttpClientResult 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,
HttpClientResult requestResult) {
// Must be implemented if testAsync is true
throw new UnsupportedOperationException()
}
@Shared
def junitTest = new AbstractHttpClientTest() {
@Override
buildRequest(String method, URI uri, Map headers) {
return HttpClientTest.this.buildRequest(method, uri, headers)
}
@Override
int sendRequest(def request, String method, URI uri, Map headers) {
return HttpClientTest.this.sendRequest(request, method, uri, headers)
}
@Override
void sendRequestWithCallback(def request, String method, URI uri, Map headers,
HttpClientResult requestResult) {
HttpClientTest.this.sendRequestWithCallback(request, method, uri, headers, requestResult)
}
@Override
protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
optionsBuilder.setHttpAttributes(HttpClientTest.this.&httpAttributes)
optionsBuilder.setResponseCodeOnRedirectError(responseCodeOnRedirectError())
optionsBuilder.setClientSpanErrorMapper(HttpClientTest.this.&clientSpanError)
optionsBuilder.setSingleConnectionFactory(HttpClientTest.this.&createSingleConnection)
optionsBuilder.setExpectedClientSpanNameMapper(HttpClientTest.this.&expectedClientSpanName)
optionsBuilder.setTestWithClientParent(HttpClientTest.this.testWithClientParent())
optionsBuilder.setTestRedirects(HttpClientTest.this.testRedirects())
optionsBuilder.setTestCircularRedirects(HttpClientTest.this.testCircularRedirects())
optionsBuilder.setMaxRedirects(HttpClientTest.this.maxRedirects())
optionsBuilder.setTestReusedRequest(HttpClientTest.this.testReusedRequest())
optionsBuilder.setTestConnectionFailure(HttpClientTest.this.testConnectionFailure())
optionsBuilder.setTestReadTimeout(HttpClientTest.this.testReadTimeout())
optionsBuilder.setTestRemoteConnection(HttpClientTest.this.testRemoteConnection())
optionsBuilder.setTestHttps(HttpClientTest.this.testHttps())
optionsBuilder.setTestCallback(HttpClientTest.this.testCallback())
optionsBuilder.setTestCallbackWithParent(HttpClientTest.this.testCallbackWithParent())
optionsBuilder.setTestErrorWithCallback(HttpClientTest.this.testErrorWithCallback())
optionsBuilder.setTestNonStandardHttpMethod(HttpClientTest.this.testNonStandardHttpMethod())
}
}
@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 "request with non-standard http method"() {
assumeTrue(testNonStandardHttpMethod())
expect:
junitTest.requestWithNonStandardHttpMethod()
}
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:
try {
junitTest.requestWithCallbackAndParent()
} catch (Exception ignored) {
}
}
def "trace request with callback and no parent"() {
assumeTrue(testCallback())
expect:
try {
junitTest.requestWithCallbackAndNoParent()
} catch (Exception ignored) {
}
}
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 "test captured HTTP headers"() {
assumeTrue(testCapturedHttpHeaders())
expect:
junitTest.captureHttpHeaders()
}
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()
}
def "http client metrics"() {
expect:
junitTest.httpClientMetrics()
}
/**
* 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"() {
expect:
junitTest.highConcurrency()
}
def "high concurrency test with callback"() {
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 HttpConstants._OTHER == method ? "HTTP" : method
}
Integer responseCodeOnRedirectError() {
return 302
}
/** 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 testCapturedHttpHeaders() {
true
}
boolean testConnectionFailure() {
true
}
boolean testRemoteConnection() {
true
}
boolean testReadTimeout() {
true
}
boolean testHttps() {
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 testErrorWithCallback() {
return true
}
boolean testNonStandardHttpMethod() {
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, null)
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)
}
}