Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test whether http client span ends after headers or body is received #12149

Merged
merged 6 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest;
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult;
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
Expand Down Expand Up @@ -69,7 +70,7 @@ HttpAsyncClient getClient(URI uri) {
}

@Nested
class ApacheClientUriRequestTest extends AbstractHttpClientTest<HttpUriRequest> {
class ApacheClientUriRequestTest extends AbstractTest {

@Override
public HttpUriRequest buildRequest(String method, URI uri, Map<String, String> headers) {
Expand All @@ -95,7 +96,7 @@ public void sendRequestWithCallback(
}

@Nested
class ApacheClientHostRequestTest extends AbstractHttpClientTest<HttpUriRequest> {
class ApacheClientHostRequestTest extends AbstractTest {

@Override
public HttpUriRequest buildRequest(String method, URI uri, Map<String, String> headers) {
Expand Down Expand Up @@ -129,7 +130,7 @@ public void sendRequestWithCallback(
}

@Nested
class ApacheClientHostAbsoluteUriRequestTest extends AbstractHttpClientTest<HttpUriRequest> {
class ApacheClientHostAbsoluteUriRequestTest extends AbstractTest {

@Override
public HttpUriRequest buildRequest(String method, URI uri, Map<String, String> headers) {
Expand Down Expand Up @@ -161,6 +162,15 @@ public void sendRequestWithCallback(
}
}

abstract static class AbstractTest extends AbstractHttpClientTest<HttpUriRequest> {

@Override
protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
super.configure(optionsBuilder);
optionsBuilder.spanEndsAfterBody();
}
}

HttpUriRequest configureRequest(HttpUriRequest request, Map<String, String> headers) {
request.addHeader("user-agent", "httpasyncclient");
headers.forEach((key, value) -> request.setHeader(new BasicHeader(key, value)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult;
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions;
import java.net.URI;
import java.util.Map;
import java.util.concurrent.CancellationException;
Expand Down Expand Up @@ -86,6 +87,13 @@ protected HttpContext getContext() {
}

abstract class AbstractTest extends AbstractApacheHttpClientTest<SimpleHttpRequest> {

@Override
protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
super.configure(optionsBuilder);
optionsBuilder.spanEndsAfterBody();
}

@Override
public SimpleHttpRequest buildRequest(String method, URI uri, Map<String, String> headers) {
SimpleHttpRequest httpRequest = super.buildRequest(method, uri, headers);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
optionsBuilder.disableTestReusedRequest();
// can only use methods from HttpMethod enum
optionsBuilder.disableTestNonStandardHttpMethod();
optionsBuilder.spanEndsAfterBody();
// armeria uses http/2
optionsBuilder.setHttpProtocolVersion(
uri -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ public void onThrowable(Throwable throwable) {

@Override
protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
optionsBuilder.spanEndsAfterBody();
optionsBuilder.disableTestRedirects();

// disable read timeout test for non latest because it is flaky with 1.9.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,6 @@ public void onThrowable(Throwable throwable) {
@Override
protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
optionsBuilder.disableTestRedirects();
optionsBuilder.spanEndsAfterBody();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
optionsBuilder.setHttpAttributes(ClientTest::getHttpAttributes);
optionsBuilder.setExpectedClientSpanNameMapper(ClientTest::getExpectedClientSpanName);
optionsBuilder.disableTestRedirects();
optionsBuilder.spanEndsAfterBody();
optionsBuilder.setClientSpanErrorMapper(
(uri, error) -> {
// all errors should be wrapped in RuntimeExceptions due to how we run things in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ public int sendRequest(HttpRequest request, String method, URI uri, Map<String,
throws Exception {
HttpResponse response = sendRequest(request);
// read request body to avoid broken pipe errors on the server side
response.parseAsString();
// skip reading body of long-request to make the test a bit faster
if (!uri.toString().contains("/long-request")) {
response.parseAsString();
trask marked this conversation as resolved.
Show resolved Hide resolved
}
return response.getStatusCode();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ public int sendRequest(
Span parentSpan = Span.current();
InputStream stream = connection.getInputStream();
assertThat(Span.current()).isEqualTo(parentSpan);
readLines(stream);
// skip reading body of long-request to make the test a bit faster
if (!uri.toString().contains("/long-request")) {
readLines(stream);
trask marked this conversation as resolved.
Show resolved Hide resolved
}
stream.close();
return connection.getResponseCode();
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
// TODO nested client span is not created, but context is still injected
// which is not what the test expects
optionsBuilder.disableTestWithClientParent();
optionsBuilder.spanEndsAfterBody();

optionsBuilder.setHttpAttributes(
uri -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
// jetty 12 does not support to reuse request
// use request.send() twice will block the program infinitely
optionsBuilder.disableTestReusedRequest();
optionsBuilder.spanEndsAfterBody();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public void after() throws Exception {
@Override
protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
optionsBuilder.disableTestRedirects();
optionsBuilder.spanEndsAfterBody();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,6 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
optionsBuilder.disableTestCallback();
// Circular Redirects are not explicitly handled by jodd-http
optionsBuilder.disableTestCircularRedirects();
optionsBuilder.spanEndsAfterBody();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,15 @@ import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.opentelemetry.api.trace.SpanKind
import io.opentelemetry.context.Context
import io.opentelemetry.extension.kotlin.asContextElement
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.HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES
import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat
import io.opentelemetry.sdk.testing.assertj.TraceAssert
import io.opentelemetry.sdk.trace.data.StatusData
import io.opentelemetry.semconv.NetworkAttributes
import kotlinx.coroutines.*
import org.junit.jupiter.api.Test
import java.net.URI
import java.util.function.Consumer

abstract class AbstractKtorHttpClientTest : AbstractHttpClientTest<HttpRequestBuilder>() {

Expand Down Expand Up @@ -69,6 +63,7 @@ abstract class AbstractKtorHttpClientTest : AbstractHttpClientTest<HttpRequestBu
// this instrumentation creates a span per each physical request
// related issue https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/5722
disableTestRedirects()
spanEndsAfterBody()

setHttpAttributes { DEFAULT_HTTP_ATTRIBUTES - setOf(NetworkAttributes.NETWORK_PROTOCOL_VERSION) }

Expand All @@ -77,24 +72,4 @@ abstract class AbstractKtorHttpClientTest : AbstractHttpClientTest<HttpRequestBu
}
}
}

@Test
fun checkSpanEndsAfterBodyReceived() {
val method = "GET"
val path = "/long-request"
val uri = resolveAddress(path)
val responseCode = doRequest(method, uri)

assertThat(responseCode).isEqualTo(200)

testing.waitAndAssertTraces(
Consumer { trace: TraceAssert ->
val span = trace.getSpan(0)
assertThat(span).hasKind(SpanKind.CLIENT).hasStatus(StatusData.unset())
assertThat(span.endEpochNanos - span.startEpochNanos >= 1_000_000_000)
.describedAs("Span duration should be at least 1000ms")
.isTrue()
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.extension.RegisterExtension;

class Netty40ClientTest extends AbstractHttpClientTest<DefaultFullHttpRequest> {
Expand All @@ -44,15 +45,12 @@ class Netty40ClientTest extends AbstractHttpClientTest<DefaultFullHttpRequest> {
static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent();

private final EventLoopGroup eventLoopGroup = new NioEventLoopGroup();

private final Bootstrap bootstrap = buildBootstrap(false);

private final Bootstrap readTimeoutBootstrap = buildBootstrap(true);

void cleanupSpec() {
if (eventLoopGroup != null) {
eventLoopGroup.shutdownGracefully();
}
@AfterAll
void cleanup() {
eventLoopGroup.shutdownGracefully();
}

Bootstrap buildBootstrap(boolean readTimeout) {
Expand Down Expand Up @@ -137,7 +135,7 @@ public void sendRequestWithCallback(
protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
optionsBuilder.disableTestRedirects();
optionsBuilder.disableTestHttps();
optionsBuilder.disableTestReadTimeout();
optionsBuilder.spanEndsAfterBody();

optionsBuilder.setExpectedClientSpanNameMapper(Netty40ClientTest::expectedClientSpanName);
optionsBuilder.setHttpAttributes(Netty40ClientTest::httpAttributes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
this::configureChannel));

optionsBuilder.disableTestRedirects();
optionsBuilder.spanEndsAfterBody();
}

private static Set<AttributeKey<?>> getHttpAttributes(URI uri) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,10 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
optionsBuilder.setHttpAttributes(this::computeHttpAttributes);

optionsBuilder.disableTestRedirects();

// these tests will pass, but they don't really test anything since REQUEST is Void
optionsBuilder.disableTestReusedRequest();

optionsBuilder.spanEndsAfterBody();
}

protected Set<AttributeKey<?>> computeHttpAttributes(URI uri) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
optionsBuilder.disableTestReusedRequest();

optionsBuilder.setHttpAttributes(this::getHttpAttributes);

optionsBuilder.spanEndsAfterBody();
}

protected Set<AttributeKey<?>> getHttpAttributes(URI uri) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public void sendRequestWithCallback(
@Override
protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
optionsBuilder.disableTestRedirects();
optionsBuilder.spanEndsAfterBody();

optionsBuilder.setExpectedClientSpanNameMapper(
(uri, method) -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ public void sendRequestWithCallback(
protected void configure(HttpClientTestOptions.Builder optionsBuilder) {
optionsBuilder.markAsLowLevelInstrumentation();
optionsBuilder.setMaxRedirects(52);
optionsBuilder.spanEndsAfterBody();

// TODO: remove this test altogether? this scenario is (was) only implemented in reactor-netty,
// all other HTTP clients worked in a different way
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,10 @@ abstract class HttpClientTest<REQUEST> extends InstrumentationSpecification {
junitTest.highConcurrencyOnSingleConnection()
}

def "http client span ends after headers are received"() {
junitTest.spanEndsAfterHeadersReceived()
}

// ideally private, but then groovy closures in this class cannot find them
final int doRequest(String method, URI uri, Map<String, String> headers = [:]) {
def request = buildRequest(method, uri, headers)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -958,6 +959,75 @@ void highConcurrencyOnSingleConnection() {
pool.shutdown();
}

@Test
void spanEndsAfterBodyReceived() throws Exception {
assumeTrue(options.isSpanEndsAfterBody());

String method = "GET";
URI uri = resolveAddress("/long-request");

int responseCode =
doRequest(
method,
uri,
// the time that server waits before completing the response
Collections.singletonMap("delay", String.valueOf(TimeUnit.SECONDS.toMillis(1))));

assertThat(responseCode).isEqualTo(200);

testing.waitAndAssertTraces(
trace -> {
trace.hasSpansSatisfyingExactly(
span ->
assertClientSpan(span, uri, method, 200, null)
.hasNoParent()
.hasStatus(StatusData.unset()),
span -> assertServerSpan(span).hasParent(trace.getSpan(0)));
SpanData span = trace.getSpan(0);
// make sure the span is at least as long as the delay we set when sending the request
assertThat(
span.getEndEpochNanos() - span.getStartEpochNanos()
>= TimeUnit.SECONDS.toNanos(1))
trask marked this conversation as resolved.
Show resolved Hide resolved
.describedAs("Span duration should be at least 1s")
.isTrue();
});
}

@Test
void spanEndsAfterHeadersReceived() throws Exception {
assumeTrue(options.isSpanEndsAfterHeaders());

String method = "GET";
URI uri = resolveAddress("/long-request");

int responseCode =
doRequest(
method,
uri,
// the time that server waits before completing the response, we expect the response
// headers to arrive much sooner
Collections.singletonMap("delay", String.valueOf(TimeUnit.SECONDS.toMillis(2))));

assertThat(responseCode).isEqualTo(200);

testing.waitAndAssertTraces(
trace -> {
trace.hasSpansSatisfyingExactly(
span ->
assertClientSpan(span, uri, method, 200, null)
.hasNoParent()
.hasStatus(StatusData.unset()),
span -> assertServerSpan(span).hasParent(trace.getSpan(0)));
SpanData span = trace.getSpan(0);
// verify that the span length is less than the delay used to complete the response body
assertThat(
span.getEndEpochNanos() - span.getStartEpochNanos()
<= TimeUnit.SECONDS.toNanos(2))
.describedAs("Span duration should be less than 2s")
.isTrue();
});
}

// Visible for spock bridge.
SpanDataAssert assertClientSpan(
SpanDataAssert span,
Expand Down Expand Up @@ -1053,7 +1123,7 @@ static SpanDataAssert assertServerSpan(SpanDataAssert span) {
return span.hasName("test-http-server").hasKind(SpanKind.SERVER);
}

protected int doRequest(String method, URI uri) throws Exception {
private int doRequest(String method, URI uri) throws Exception {
return doRequest(method, uri, Collections.emptyMap());
}

Expand Down
Loading
Loading