All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest Maven / Gradle / Ivy

There is a newer version: 2.10.0-alpha
Show newest version
/*
 * Copyright The OpenTelemetry Authors
 * SPDX-License-Identifier: Apache-2.0
 */

package io.opentelemetry.instrumentation.testing.junit.http;

import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS;
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_PARAMETERS;
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR;
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION;
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD;
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND;
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM;
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT;
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
import static org.junit.jupiter.api.Assumptions.assumeFalse;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.context.propagation.TextMapSetter;
import io.opentelemetry.instrumentation.api.internal.HttpConstants;
import io.opentelemetry.instrumentation.testing.GlobalTraceUtil;
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.ClientAttributes;
import io.opentelemetry.semconv.ErrorAttributes;
import io.opentelemetry.semconv.HttpAttributes;
import io.opentelemetry.semconv.NetworkAttributes;
import io.opentelemetry.semconv.ServerAttributes;
import io.opentelemetry.semconv.UrlAttributes;
import io.opentelemetry.semconv.UserAgentAttributes;
import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest;
import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse;
import io.opentelemetry.testing.internal.armeria.common.HttpData;
import io.opentelemetry.testing.internal.armeria.common.HttpHeaderNames;
import io.opentelemetry.testing.internal.armeria.common.HttpMethod;
import io.opentelemetry.testing.internal.armeria.common.HttpRequest;
import io.opentelemetry.testing.internal.armeria.common.HttpRequestBuilder;
import io.opentelemetry.testing.internal.armeria.common.MediaType;
import io.opentelemetry.testing.internal.armeria.common.QueryParams;
import io.opentelemetry.testing.internal.armeria.common.RequestHeaders;
import io.opentelemetry.testing.internal.io.netty.bootstrap.Bootstrap;
import io.opentelemetry.testing.internal.io.netty.buffer.Unpooled;
import io.opentelemetry.testing.internal.io.netty.channel.Channel;
import io.opentelemetry.testing.internal.io.netty.channel.ChannelHandlerContext;
import io.opentelemetry.testing.internal.io.netty.channel.ChannelInitializer;
import io.opentelemetry.testing.internal.io.netty.channel.ChannelOption;
import io.opentelemetry.testing.internal.io.netty.channel.ChannelPipeline;
import io.opentelemetry.testing.internal.io.netty.channel.EventLoopGroup;
import io.opentelemetry.testing.internal.io.netty.channel.SimpleChannelInboundHandler;
import io.opentelemetry.testing.internal.io.netty.channel.nio.NioEventLoopGroup;
import io.opentelemetry.testing.internal.io.netty.channel.socket.SocketChannel;
import io.opentelemetry.testing.internal.io.netty.channel.socket.nio.NioSocketChannel;
import io.opentelemetry.testing.internal.io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.opentelemetry.testing.internal.io.netty.handler.codec.http.DefaultHttpResponse;
import io.opentelemetry.testing.internal.io.netty.handler.codec.http.HttpClientCodec;
import io.opentelemetry.testing.internal.io.netty.handler.codec.http.HttpObject;
import io.opentelemetry.testing.internal.io.netty.handler.codec.http.HttpVersion;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.assertj.core.api.AssertAccess;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;

public abstract class AbstractHttpServerTest extends AbstractHttpServerUsingTest {
  public static final String TEST_REQUEST_HEADER = "X-Test-Request";
  public static final String TEST_RESPONSE_HEADER = "X-Test-Response";

  private final HttpServerTestOptions options = new HttpServerTestOptions();

  @BeforeAll
  void setupOptions() {
    options.expectedServerSpanNameMapper = this::expectedServerSpanName;
    options.expectedHttpRoute = this::expectedHttpRoute;

    configure(options);

    startServer();
  }

  @AfterAll
  void cleanup() {
    cleanupServer();
  }

  @Override
  protected final String getContextPath() {
    return options.contextPath;
  }

  protected void configure(HttpServerTestOptions options) {}

  public static  T controller(ServerEndpoint endpoint, Supplier closure) {
    assert Span.current().getSpanContext().isValid() : "Controller should have a parent span.";
    if (endpoint == NOT_FOUND) {
      return closure.get();
    }
    return GlobalTraceUtil.runWithSpan("controller", () -> closure.get());
  }

  protected AggregatedHttpRequest request(ServerEndpoint uri, String method) {
    return AggregatedHttpRequest.of(
        HttpMethod.valueOf(method), resolveAddress(uri, getProtocolPrefix()));
  }

  private String getProtocolPrefix() {
    return options.useHttp2 ? "h2c://" : "h1c://";
  }

  @ParameterizedTest
  @ValueSource(ints = {1, 4, 50})
  void successfulGetRequest(int count) {
    String method = "GET";
    AggregatedHttpRequest request = request(SUCCESS, method);
    List responses = new ArrayList<>();
    for (int i = 0; i < count; i++) {
      responses.add(client.execute(request).aggregate().join());
    }

    for (AggregatedHttpResponse response : responses) {
      assertThat(response.status().code()).isEqualTo(SUCCESS.getStatus());
      assertThat(response.contentUtf8()).isEqualTo(SUCCESS.getBody());
      assertResponseHasCustomizedHeaders(response, SUCCESS, null);
    }

    assertTheTraces(count, null, null, null, method, SUCCESS);
  }

  @Test
  void successfulGetRequestWithParent() {
    String method = "GET";
    String traceId = "00000000000000000000000000000123";
    String parentId = "0000000000000456";
    AggregatedHttpRequest request =
        AggregatedHttpRequest.of(
            // intentionally sending mixed-case "tracePARENT" to make sure that TextMapGetters are
            // not case-sensitive
            request(SUCCESS, method).headers().toBuilder()
                .set("tracePARENT", "00-" + traceId + "-" + parentId + "-01")
                .build());
    AggregatedHttpResponse response = client.execute(request).aggregate().join();

    assertThat(response.status().code()).isEqualTo(SUCCESS.getStatus());
    assertThat(response.contentUtf8()).isEqualTo(SUCCESS.getBody());

    String spanId = assertResponseHasCustomizedHeaders(response, SUCCESS, traceId);
    assertTheTraces(1, traceId, parentId, spanId, "GET", SUCCESS);
  }

  @Test
  void tracingHeaderIsCaseInsensitive() {
    String method = "GET";
    String traceId = "00000000000000000000000000000123";
    String parentId = "0000000000000456";
    AggregatedHttpRequest request =
        AggregatedHttpRequest.of(
            request(SUCCESS, method).headers().toBuilder()
                .set("TRACEPARENT", "00-" + traceId + "-" + parentId + "-01")
                .build());
    AggregatedHttpResponse response = client.execute(request).aggregate().join();

    assertThat(response.status().code()).isEqualTo(SUCCESS.getStatus());
    assertThat(response.contentUtf8()).isEqualTo(SUCCESS.getBody());

    String spanId = assertResponseHasCustomizedHeaders(response, SUCCESS, traceId);
    assertTheTraces(1, traceId, parentId, spanId, "GET", SUCCESS);
  }

  @ParameterizedTest
  @MethodSource("provideServerEndpoints")
  void requestWithQueryString(ServerEndpoint endpoint) {
    String method = "GET";
    AggregatedHttpRequest request = request(endpoint, method);
    AggregatedHttpResponse response = client.execute(request).aggregate().join();

    assertThat(response.status().code()).isEqualTo(endpoint.getStatus());
    assertThat(response.contentUtf8()).isEqualTo(endpoint.getBody());

    String spanId = assertResponseHasCustomizedHeaders(response, endpoint, null);
    assertTheTraces(1, null, null, spanId, method, endpoint);
  }

  private static Stream provideServerEndpoints() {
    return Stream.of(ServerEndpoint.SUCCESS, ServerEndpoint.QUERY_PARAM);
  }

  @Test
  void requestWithRedirect() {
    assumeTrue(options.testRedirect);

    String method = "GET";
    AggregatedHttpRequest request = request(REDIRECT, method);
    AggregatedHttpResponse response = client.execute(request).aggregate().join();

    assertThat(response.status().code()).isEqualTo(REDIRECT.getStatus());
    assertThat(response.headers().get("location"))
        .satisfiesAnyOf(
            location -> assertThat(location).isEqualTo(REDIRECT.getBody()),
            location ->
                assertThat(new URI(location).normalize().toString())
                    .isEqualTo(address.resolve(REDIRECT.getBody()).toString()));

    String spanId = assertResponseHasCustomizedHeaders(response, REDIRECT, null);
    assertTheTraces(1, null, null, spanId, method, REDIRECT);
  }

  @Test
  void requestWithError() {
    assumeTrue(options.testError);

    String method = "GET";
    AggregatedHttpRequest request = request(ERROR, method);
    AggregatedHttpResponse response = client.execute(request).aggregate().join();

    assertThat(response.status().code()).isEqualTo(ERROR.getStatus());
    if (options.testErrorBody) {
      assertThat(response.contentUtf8()).isEqualTo(ERROR.getBody());
    }

    String spanId = assertResponseHasCustomizedHeaders(response, ERROR, null);
    assertTheTraces(1, null, null, spanId, method, ERROR);
  }

  @Test
  void requestWithException() {
    assumeTrue(options.testException);

    // async servlet tests may produce uncaught exceptions
    // awaitility rethrows uncaught exceptions while it is waiting on a condition
    Awaitility.doNotCatchUncaughtExceptionsByDefault();
    try {
      String method = "GET";
      AggregatedHttpRequest request = request(EXCEPTION, method);
      AggregatedHttpResponse response = client.execute(request).aggregate().join();

      assertThat(response.status().code()).isEqualTo(EXCEPTION.getStatus());

      String spanId = assertResponseHasCustomizedHeaders(response, EXCEPTION, null);
      assertTheTraces(1, null, null, spanId, method, EXCEPTION);
    } finally {
      Awaitility.reset();
    }
  }

  @Test
  void requestForNotFound() {
    assumeTrue(options.testNotFound);

    String method = "GET";
    AggregatedHttpRequest request = request(NOT_FOUND, method);
    AggregatedHttpResponse response = client.execute(request).aggregate().join();

    assertThat(response.status().code()).isEqualTo(NOT_FOUND.getStatus());

    String spanId = assertResponseHasCustomizedHeaders(response, NOT_FOUND, null);
    assertTheTraces(1, null, null, spanId, method, NOT_FOUND);
  }

  @Test
  void requestWithPathParameter() {
    assumeTrue(options.testPathParam);

    String method = "GET";
    AggregatedHttpRequest request = request(PATH_PARAM, method);
    AggregatedHttpResponse response = client.execute(request).aggregate().join();

    assertThat(response.status().code()).isEqualTo(PATH_PARAM.getStatus());
    assertThat(response.contentUtf8()).isEqualTo(PATH_PARAM.getBody());

    String spanId = assertResponseHasCustomizedHeaders(response, PATH_PARAM, null);
    assertTheTraces(1, null, null, spanId, method, PATH_PARAM);
  }

  @Test
  void captureHttpHeaders() {
    assumeTrue(options.testCaptureHttpHeaders);

    AggregatedHttpRequest request =
        AggregatedHttpRequest.of(
            request(CAPTURE_HEADERS, "GET").headers().toBuilder()
                .add(TEST_REQUEST_HEADER, "test")
                .build());
    AggregatedHttpResponse response = client.execute(request).aggregate().join();

    assertThat(response.status().code()).isEqualTo(CAPTURE_HEADERS.getStatus());
    assertThat(response.contentUtf8()).isEqualTo(CAPTURE_HEADERS.getBody());
    assertThat(response.headers().get("X-Test-Response")).isEqualTo("test");

    String spanId = assertResponseHasCustomizedHeaders(response, CAPTURE_HEADERS, null);
    assertTheTraces(1, null, null, spanId, "GET", CAPTURE_HEADERS);
  }

  @Test
  void captureRequestParameters() {
    assumeTrue(options.testCaptureRequestParameters);

    QueryParams formBody = QueryParams.builder().add("test-parameter", "test value õäöü").build();
    AggregatedHttpRequest request =
        AggregatedHttpRequest.of(
            RequestHeaders.builder(
                    HttpMethod.POST, resolveAddress(CAPTURE_PARAMETERS, getProtocolPrefix()))
                .contentType(MediaType.FORM_DATA)
                .build(),
            HttpData.ofUtf8(formBody.toQueryString()));
    AggregatedHttpResponse response = client.execute(request).aggregate().join();

    assertThat(response.status().code()).isEqualTo(CAPTURE_PARAMETERS.getStatus());
    assertThat(response.contentUtf8()).isEqualTo(CAPTURE_PARAMETERS.getBody());

    String spanId = assertResponseHasCustomizedHeaders(response, CAPTURE_PARAMETERS, null);
    assertTheTraces(1, null, null, spanId, "POST", CAPTURE_PARAMETERS);
  }

  @Test
  void httpServerMetrics() {
    String method = "GET";
    AggregatedHttpRequest request = request(SUCCESS, method);
    AggregatedHttpResponse response = client.execute(request).aggregate().join();

    assertThat(response.status().code()).isEqualTo(SUCCESS.getStatus());
    assertThat(response.contentUtf8()).isEqualTo(SUCCESS.getBody());

    AtomicReference instrumentationName = new AtomicReference<>();
    testing.waitAndAssertTraces(
        trace -> {
          instrumentationName.set(trace.getSpan(0).getInstrumentationScopeInfo().getName());
          trace.anySatisfy(
              spanData -> assertServerSpan(assertThat(spanData), method, SUCCESS, SUCCESS.status));
        });

    testing.waitAndAssertMetrics(
        instrumentationName.get(),
        "http.server.request.duration",
        metrics ->
            metrics.anySatisfy(
                metric ->
                    assertThat(metric)
                        .hasDescription("Duration of HTTP server requests.")
                        .hasUnit("s")
                        .hasHistogramSatisfying(
                            histogram ->
                                histogram.hasPointsSatisfying(
                                    point -> point.hasSumGreaterThan(0.0)))));
  }

  /**
   * This test fires a bunch of parallel request to the fixed backend endpoint. That endpoint is
   * supposed to create a new child span in the context of the SERVER span. That child span is
   * expected to have an attribute called "test.request.id". The value of that attribute should be
   * the value of request's parameter called "id".
   *
   * 

This test then asserts that there is the correct number of traces (one per request executed) * and that each trace has exactly three spans and both first and the last spans have * "test.request.id" attribute with equal value. Server span is not going to have that attribute * because it is not under the control of this test. * *

This way we verify that child span created by the server actually corresponds to the client * request. */ @Test void highConcurrency() throws InterruptedException { int count = 100; ServerEndpoint endpoint = INDEXED_CHILD; CountDownLatch latch = new CountDownLatch(count); TextMapPropagator propagator = GlobalOpenTelemetry.getPropagators().getTextMapPropagator(); TextMapSetter setter = HttpRequestBuilder::header; for (int i = 0; i < count; i++) { int index = i; HttpRequestBuilder request = HttpRequest.builder() // Force HTTP/1 via h1c so upgrade requests don't show up as traces .get(endpoint.resolvePath(address).toString().replace("http://", getProtocolPrefix())) .queryParam(ServerEndpoint.ID_PARAMETER_NAME, index); testing.runWithSpan( "client " + index, () -> { Span.current().setAttribute(ServerEndpoint.ID_ATTRIBUTE_NAME, index); propagator.inject(Context.current(), request, setter); client .execute(request.build()) .aggregate() .whenComplete((result, throwable) -> latch.countDown()); }); } latch.await(); assertHighConcurrency(count); } @Test void httpPipelining() throws InterruptedException { assumeTrue(options.testHttpPipelining); // test uses http 1.1 assumeFalse(options.useHttp2); int count = 10; CountDownLatch countDownLatch = new CountDownLatch(count); ServerEndpoint endpoint = INDEXED_CHILD; TextMapPropagator propagator = GlobalOpenTelemetry.getPropagators().getTextMapPropagator(); TextMapSetter setter = (request, key, value) -> request.headers().set(key, value); EventLoopGroup eventLoopGroup = new NioEventLoopGroup(); try { Bootstrap bootstrap = buildBootstrap(eventLoopGroup); Channel channel = bootstrap.connect(address.getHost(), port).sync().channel(); channel .pipeline() .addLast( new SimpleChannelInboundHandler() { @Override protected void channelRead0( ChannelHandlerContext channelHandlerContext, HttpObject httpObject) { if (httpObject instanceof DefaultHttpResponse) { countDownLatch.countDown(); } } }); for (int i = 0; i < count; i++) { int index = i; String target = endpoint.resolvePath(address).getPath().toString() + "?" + ServerEndpoint.ID_PARAMETER_NAME + "=" + index; testing.runWithSpan( "client " + index, () -> { Span.current().setAttribute(ServerEndpoint.ID_ATTRIBUTE_NAME, index); DefaultFullHttpRequest request = new DefaultFullHttpRequest( HttpVersion.HTTP_1_1, io.opentelemetry.testing.internal.io.netty.handler.codec.http.HttpMethod .valueOf("GET"), target, Unpooled.EMPTY_BUFFER); request.headers().set(HttpHeaderNames.HOST, address.getHost() + ":" + port); request.headers().set(HttpHeaderNames.USER_AGENT, TEST_USER_AGENT); request.headers().set(HttpHeaderNames.X_FORWARDED_FOR, TEST_CLIENT_IP); propagator.inject(Context.current(), request, setter); channel.writeAndFlush(request); }); } countDownLatch.await(30, TimeUnit.SECONDS); assertHighConcurrency(count); } finally { eventLoopGroup.shutdownGracefully().await(10, TimeUnit.SECONDS); } } @Test void requestWithNonStandardHttpMethod() throws InterruptedException { assumeTrue(options.testNonStandardHttpMethod); // test uses http 1.1 assumeFalse(options.useHttp2); EventLoopGroup eventLoopGroup = new NioEventLoopGroup(); try { Bootstrap bootstrap = buildBootstrap(eventLoopGroup); Channel channel = bootstrap.connect(address.getHost(), port).sync().channel(); String method = "TEST"; DefaultFullHttpRequest request = new DefaultFullHttpRequest( HttpVersion.HTTP_1_1, new io.opentelemetry.testing.internal.io.netty.handler.codec.http.HttpMethod(method), SUCCESS.resolvePath(address).getPath(), Unpooled.EMPTY_BUFFER); request.headers().set(HttpHeaderNames.HOST, address.getHost() + ":" + port); request.headers().set(HttpHeaderNames.USER_AGENT, TEST_USER_AGENT); request.headers().set(HttpHeaderNames.X_FORWARDED_FOR, TEST_CLIENT_IP); testing .getOpenTelemetry() .getPropagators() .getTextMapPropagator() .inject( Context.current(), request, (carrier, key, value) -> carrier.headers().set(key, value)); channel.writeAndFlush(request); // TODO: add stricter assertions; could be difficult with the groovy code still in place // though testing.waitAndAssertTraces( trace -> trace.anySatisfy( span -> assertServerSpan( assertThat(span), HttpConstants._OTHER, SUCCESS, options.responseCodeOnNonStandardHttpMethod) .hasAttribute(HttpAttributes.HTTP_REQUEST_METHOD_ORIGINAL, method))); } finally { eventLoopGroup.shutdownGracefully().await(10, TimeUnit.SECONDS); } } private static Bootstrap buildBootstrap(EventLoopGroup eventLoopGroup) { Bootstrap bootstrap = new Bootstrap(); bootstrap .group(eventLoopGroup) .channel(NioSocketChannel.class) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) TimeUnit.SECONDS.toMillis(10)) .handler( new ChannelInitializer() { @Override protected void initChannel(SocketChannel socketChannel) { ChannelPipeline pipeline = socketChannel.pipeline(); pipeline.addLast(new HttpClientCodec()); } }); return bootstrap; } protected void assertHighConcurrency(int count) { ServerEndpoint endpoint = INDEXED_CHILD; List> assertions = new ArrayList<>(); for (int i = 0; i < count; i++) { assertions.add( trace -> { SpanData rootSpan = trace.getSpan(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("client ".length())); List> spanAssertions = new ArrayList<>(); spanAssertions.add( span -> span.hasName(rootSpan.getName()) .hasKind(SpanKind.INTERNAL) .hasNoParent() .hasAttributesSatisfyingExactly( equalTo( AttributeKey.longKey(ServerEndpoint.ID_ATTRIBUTE_NAME), requestId))); spanAssertions.add( span -> assertIndexedServerSpan(span, requestId).hasParent(rootSpan)); if (options.hasHandlerSpan.test(endpoint)) { spanAssertions.add( span -> assertHandlerSpan(span, "GET", endpoint).hasParent(trace.getSpan(1))); } int parentIndex = spanAssertions.size() - 1; spanAssertions.add( span -> assertIndexedControllerSpan(span, requestId) .hasParent(trace.getSpan(parentIndex))); trace.hasSpansSatisfyingExactly(spanAssertions); }); } testing.waitAndAssertTraces(assertions); } protected String assertResponseHasCustomizedHeaders( AggregatedHttpResponse response, ServerEndpoint endpoint, String expectedTraceId) { if (!options.hasResponseCustomizer.test(endpoint)) { return null; } String responseHeaderTraceId = response.headers().get("x-test-traceid"); String responseHeaderSpanId = response.headers().get("x-test-spanid"); if (expectedTraceId != null) { assertThat(responseHeaderTraceId).matches(expectedTraceId); } else { assertThat(responseHeaderTraceId).isNotNull(); } assertThat(responseHeaderSpanId).isNotNull(); return responseHeaderSpanId; } // NOTE: this method does not currently implement asserting all the span types that groovy // HttpServerTest does protected void assertTheTraces( int size, String traceId, String parentId, String spanId, String method, ServerEndpoint endpoint) { List> assertions = new ArrayList<>(); for (int i = 0; i < size; i++) { assertions.add( trace -> { List> spanAssertions = new ArrayList<>(); spanAssertions.add( span -> { assertServerSpan(span, method, endpoint, endpoint.status); if (traceId != null) { span.hasTraceId(traceId); } if (spanId != null) { span.hasSpanId(spanId); } if (parentId != null) { span.hasParentSpanId(parentId); } else { span.hasNoParent(); } }); if (options.hasHandlerSpan.test(endpoint)) { spanAssertions.add( span -> { assertHandlerSpan(span, method, endpoint); span.hasParent(trace.getSpan(0)); }); } if (endpoint != NOT_FOUND) { int parentIndex = 0; if (options.hasHandlerSpan.test(endpoint)) { parentIndex = spanAssertions.size() - 1; } int finalParentIndex = parentIndex; spanAssertions.add( span -> { assertControllerSpan( span, endpoint == EXCEPTION ? options.expectedException : null); span.hasParent(trace.getSpan(finalParentIndex)); }); if (options.hasRenderSpan.test(endpoint)) { spanAssertions.add(span -> assertRenderSpan(span, method, endpoint)); } } if (options.hasResponseSpan.test(endpoint)) { int parentIndex = spanAssertions.size() - 1; spanAssertions.add( span -> assertResponseSpan(span, trace.getSpan(parentIndex), method, endpoint)); } if (options.hasErrorPageSpans.test(endpoint)) { spanAssertions.addAll(errorPageSpanAssertions(method, endpoint)); } trace.hasSpansSatisfyingExactly(spanAssertions); if (options.verifyServerSpanEndTime) { List spanData = AssertAccess.getActual(trace); if (spanData.size() > 1) { SpanData rootSpan = spanData.get(0); for (int j = 1; j < spanData.size(); j++) { assertThat(rootSpan.getEndEpochNanos()) .isGreaterThanOrEqualTo(spanData.get(j).getEndEpochNanos()); } } } }); } testing.waitAndAssertTraces(assertions); } @CanIgnoreReturnValue protected SpanDataAssert assertControllerSpan(SpanDataAssert span, Throwable expectedException) { span.hasName("controller").hasKind(SpanKind.INTERNAL); if (expectedException != null) { span.hasStatus(StatusData.error()); span.hasException(expectedException); } return span; } protected SpanDataAssert assertHandlerSpan( SpanDataAssert span, String method, ServerEndpoint endpoint) { throw new UnsupportedOperationException( "assertHandlerSpan not implemented in " + getClass().getName()); } @CanIgnoreReturnValue protected SpanDataAssert assertResponseSpan( SpanDataAssert span, SpanData parentSpan, String method, ServerEndpoint endpoint) { span.hasParent(parentSpan); return assertResponseSpan(span, method, endpoint); } protected SpanDataAssert assertResponseSpan( SpanDataAssert span, String method, ServerEndpoint endpoint) { throw new UnsupportedOperationException( "assertResponseSpan not implemented in " + getClass().getName()); } protected SpanDataAssert assertRenderSpan( SpanDataAssert span, String method, ServerEndpoint endpoint) { throw new UnsupportedOperationException( "assertRenderSpan not implemented in " + getClass().getName()); } protected List> errorPageSpanAssertions( String method, ServerEndpoint endpoint) { throw new UnsupportedOperationException( "errorPageSpanAssertions not implemented in " + getClass().getName()); } @CanIgnoreReturnValue protected SpanDataAssert assertServerSpan( SpanDataAssert span, String method, ServerEndpoint endpoint, int statusCode) { Set> httpAttributes = options.httpAttributes.apply(endpoint); String expectedRoute = options.expectedHttpRoute.apply(endpoint, method); String name = options.expectedServerSpanNameMapper.apply(endpoint, method, expectedRoute); span.hasName(name).hasKind(SpanKind.SERVER); if (statusCode >= 500) { span.hasStatus(StatusData.error()); } if (endpoint == EXCEPTION && options.hasExceptionOnServerSpan.test(endpoint)) { span.hasException(options.expectedException); } span.hasAttributesSatisfying( attrs -> { // we're opting out of these attributes in the new semconv assertThat(attrs) .doesNotContainKey(NetworkAttributes.NETWORK_TRANSPORT) .doesNotContainKey(NetworkAttributes.NETWORK_TYPE) .doesNotContainKey(NetworkAttributes.NETWORK_PROTOCOL_NAME); if (attrs.get(NetworkAttributes.NETWORK_PROTOCOL_VERSION) != null) { assertThat(attrs) .containsEntry( NetworkAttributes.NETWORK_PROTOCOL_VERSION, options.useHttp2 ? "2" : "1.1"); } assertThat(attrs).containsEntry(ServerAttributes.SERVER_ADDRESS, "localhost"); // TODO: Move to test knob rather than always treating as optional // TODO: once httpAttributes test knob is used, verify default port values if (attrs.get(ServerAttributes.SERVER_PORT) != null) { assertThat(attrs).containsEntry(ServerAttributes.SERVER_PORT, port); } if (attrs.get(NetworkAttributes.NETWORK_PEER_ADDRESS) != null) { assertThat(attrs) .containsEntry( NetworkAttributes.NETWORK_PEER_ADDRESS, options.sockPeerAddr.apply(endpoint)); } if (attrs.get(NetworkAttributes.NETWORK_PEER_PORT) != null) { assertThat(attrs) .hasEntrySatisfying( NetworkAttributes.NETWORK_PEER_PORT, value -> assertThat(value) .isInstanceOf(Long.class) .isNotEqualTo(Long.valueOf(port))); } assertThat(attrs).containsEntry(ClientAttributes.CLIENT_ADDRESS, TEST_CLIENT_IP); // client.port is opt-in assertThat(attrs).doesNotContainKey(ClientAttributes.CLIENT_PORT); assertThat(attrs).containsEntry(HttpAttributes.HTTP_REQUEST_METHOD, method); assertThat(attrs).containsEntry(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, statusCode); if (statusCode >= 500) { assertThat(attrs).containsEntry(ErrorAttributes.ERROR_TYPE, String.valueOf(statusCode)); } assertThat(attrs).containsEntry(UserAgentAttributes.USER_AGENT_ORIGINAL, TEST_USER_AGENT); assertThat(attrs).containsEntry(UrlAttributes.URL_SCHEME, "http"); if (endpoint != INDEXED_CHILD) { assertThat(attrs) .containsEntry(UrlAttributes.URL_PATH, endpoint.resolvePath(address).getPath()); if (endpoint.getQuery() != null) { assertThat(attrs).containsEntry(UrlAttributes.URL_QUERY, endpoint.getQuery()); } } if (httpAttributes.contains(HttpAttributes.HTTP_ROUTE) && expectedRoute != null) { assertThat(attrs).containsEntry(HttpAttributes.HTTP_ROUTE, expectedRoute); } if (endpoint == CAPTURE_HEADERS) { assertThat(attrs) .containsEntry("http.request.header.x-test-request", new String[] {"test"}); assertThat(attrs) .containsEntry("http.response.header.x-test-response", new String[] {"test"}); } if (endpoint == CAPTURE_PARAMETERS) { assertThat(attrs) .containsEntry( "servlet.request.parameter.test-parameter", new String[] {"test value õäöü"}); } }); return span; } @CanIgnoreReturnValue protected SpanDataAssert assertIndexedServerSpan(SpanDataAssert span, int requestId) { ServerEndpoint endpoint = INDEXED_CHILD; String method = "GET"; return assertServerSpan(span, method, endpoint, endpoint.status) .hasAttributesSatisfying( equalTo(UrlAttributes.URL_PATH, endpoint.resolvePath(address).getPath()), equalTo(UrlAttributes.URL_QUERY, "id=" + requestId)); } @CanIgnoreReturnValue protected SpanDataAssert assertIndexedControllerSpan(SpanDataAssert span, int requestId) { span.hasName("controller") .hasKind(SpanKind.INTERNAL) .hasAttributesSatisfyingExactly( equalTo(AttributeKey.longKey(ServerEndpoint.ID_ATTRIBUTE_NAME), requestId)); return span; } public String expectedServerSpanName( ServerEndpoint endpoint, String method, @Nullable String route) { return HttpServerTestOptions.DEFAULT_EXPECTED_SERVER_SPAN_NAME_MAPPER.apply( endpoint, method, route); } public String expectedHttpRoute(ServerEndpoint endpoint, String method) { // no need to compute route if we're not expecting it if (!options.httpAttributes.apply(endpoint).contains(HttpAttributes.HTTP_ROUTE)) { return null; } if (HttpConstants._OTHER.equals(method)) { return null; } if (NOT_FOUND.equals(endpoint)) { return null; } else if (PATH_PARAM.equals(endpoint)) { return options.contextPath + "/path/:id/param"; } else { return endpoint.resolvePath(address).getPath(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy