/* * Copyright The OpenTelemetry Authors * SPDX-License-Identifier: Apache-2.0 */ import io.opentelemetry.instrumentation.test.AgentTestTrait import io.opentelemetry.instrumentation.test.asserts.TraceAssert import io.opentelemetry.instrumentation.test.base.HttpServerTest import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint import io.opentelemetry.sdk.trace.data.SpanData import io.opentelemetry.semconv.trace.attributes.SemanticAttributes import spock.lang.Unroll import java.util.concurrent.TimeUnit import static io.opentelemetry.api.trace.SpanKind.INTERNAL import static io.opentelemetry.api.trace.SpanKind.SERVER import static io.opentelemetry.api.trace.StatusCode.ERROR import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS import static java.util.concurrent.TimeUnit.SECONDS import static org.junit.jupiter.api.Assumptions.assumeTrue abstract class AbstractJaxRsHttpServerTest extends HttpServerTest implements AgentTestTrait { abstract void awaitBarrier(int amount, TimeUnit timeUnit) def "test super method without @Path"() { given: def response = client.get(address.resolve("test-resource-super").toString()).aggregate().join() expect: response.status().code() == SUCCESS.status response.contentUtf8() == SUCCESS.body assertTraces(1) { trace(0, 2) { span(0) { hasNoParent() kind SERVER name "GET " + getContextPath() + "/test-resource-super" } span(1) { name "controller" kind INTERNAL childOf span(0) } } } } def "test interface method with @Path"() { assumeTrue(testInterfaceMethodWithPath()) given: def response = client.get(address.resolve("test-resource-interface/call").toString()).aggregate().join() expect: response.status().code() == SUCCESS.status response.contentUtf8() == SUCCESS.body assertTraces(1) { trace(0, 2) { span(0) { hasNoParent() kind SERVER name "GET " + getContextPath() + "/test-resource-interface/call" } span(1) { name "controller" kind INTERNAL childOf span(0) } } } } def "test sub resource locator"() { given: def response = client.get(address.resolve("test-sub-resource-locator/call/sub").toString()).aggregate().join() expect: response.status().code() == SUCCESS.status response.contentUtf8() == SUCCESS.body assertTraces(1) { trace(0, 5) { span(0) { hasNoParent() kind SERVER name "GET " + getContextPath() + "/test-sub-resource-locator/call/sub" } span(1) { name "JaxRsSubResourceLocatorTestResource.call" kind INTERNAL childOf span(0) } span(2) { name "controller" kind INTERNAL childOf span(1) } span(3) { name "SubResource.call" kind INTERNAL childOf span(0) } span(4) { name "controller" kind INTERNAL childOf span(3) } } } } @Unroll def "should handle #desc AsyncResponse"() { given: def url = address.resolve("async?action=${action}").toString() when: "async call is started" def futureResponse = client.get(url).aggregate() then: "there are no traces yet" assertTraces(0) { } when: "barrier is released and resource class sends response" awaitBarrier(1, SECONDS) def response = futureResponse.join() then: response.status().code() == statusCode bodyPredicate(response.contentUtf8()) def spanCount = 2 def hasSendError = asyncCancelHasSendError() && action == "cancel" if (hasSendError) { spanCount++ } assertTraces(1) { trace(0, spanCount) { asyncServerSpan(it, 0, url, statusCode) handlerSpan(it, 1, span(0), "asyncOp", isCancelled, isError, errorMessage) if (hasSendError) { sendErrorSpan(it, 2, span(1)) } } } where: desc | action | statusCode | bodyPredicate | isCancelled | isError | errorMessage "successful" | "succeed" | 200 | { it == "success" } | false | false | null "failing" | "throw" | 500 | { it == "failure" } | false | true | "failure" "canceled" | "cancel" | 503 | { it instanceof String } | true | false | null } @Unroll def "should handle #desc CompletionStage (JAX-RS 2.1+ only)"() { assumeTrue(shouldTestCompletableStageAsync()) given: def url = address.resolve("async-completion-stage?action=${action}").toString() when: "async call is started" def futureResponse = client.get(url).aggregate() then: "there are no traces yet" assertTraces(0) { } when: "barrier is released and resource class sends response" awaitBarrier(1, SECONDS) def response = futureResponse.join() then: response.status().code() == statusCode bodyPredicate(response.contentUtf8()) assertTraces(1) { trace(0, 2) { asyncServerSpan(it, 0, url, statusCode) handlerSpan(it, 1, span(0), "jaxRs21Async", false, isError, errorMessage) } } where: desc | action | statusCode | bodyPredicate | isError | errorMessage "successful" | "succeed" | 200 | { it == "success" } | false | null "failing" | "throw" | 500 | { it == "failure" } | true | "failure" } @Override boolean hasHandlerSpan(ServerEndpoint endpoint) { true } @Override boolean testNotFound() { false } @Override boolean testPathParam() { true } boolean testInterfaceMethodWithPath() { true } boolean asyncCancelHasSendError() { false } boolean shouldTestCompletableStageAsync() { Boolean.getBoolean("testLatestDeps") } @Override boolean hasResponseCustomizer(ServerEndpoint endpoint) { true } @Override void serverSpan(TraceAssert trace, int index, String traceID = null, String parentID = null, String method = "GET", Long responseContentLength = null, ServerEndpoint endpoint = SUCCESS, String spanID = null) { serverSpan(trace, index, traceID, parentID, spanID, method, endpoint == PATH_PARAM ? getContextPath() + "/path/{id}/param" : endpoint.resolvePath(address).path, endpoint.resolve(address), endpoint.status, endpoint.query) } void asyncServerSpan(TraceAssert trace, int index, String url, int statusCode) { def rawUrl = URI.create(url).toURL() serverSpan(trace, index, null, null, null, "GET", rawUrl.path, rawUrl.toURI(), statusCode, null) } void serverSpan(TraceAssert trace, int index, String traceID, String parentID, String spanID, String method, String path, URI fullUrl, int statusCode, String query) { trace.span(index) { name method + " " + path kind SERVER if (statusCode >= 500) { status ERROR } if (traceID != null) { traceId traceID } if (parentID != null) { parentSpanId parentID } else { hasNoParent() } if (spanID != null) { spanId spanID } attributes { "net.protocol.name" "http" "net.protocol.version" "1.1" "$SemanticAttributes.NET_HOST_NAME" fullUrl.host "$SemanticAttributes.NET_HOST_PORT" fullUrl.port "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1" "$SemanticAttributes.NET_SOCK_PEER_PORT" Long "$SemanticAttributes.NET_SOCK_HOST_ADDR" "127.0.0.1" "$SemanticAttributes.HTTP_SCHEME" fullUrl.getScheme() "$SemanticAttributes.HTTP_TARGET" fullUrl.getPath() + (fullUrl.getQuery() != null ? "?" + fullUrl.getQuery() : "") "$SemanticAttributes.HTTP_METHOD" method "$SemanticAttributes.HTTP_STATUS_CODE" statusCode "$SemanticAttributes.USER_AGENT_ORIGINAL" TEST_USER_AGENT "$SemanticAttributes.HTTP_CLIENT_IP" TEST_CLIENT_IP // Optional "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } "$SemanticAttributes.HTTP_ROUTE" path if (fullUrl.getPath().endsWith(ServerEndpoint.CAPTURE_HEADERS.getPath())) { "http.request.header.x_test_request" { it == ["test"] } "http.response.header.x_test_response" { it == ["test"] } } } } } @Override void handlerSpan(TraceAssert trace, int index, Object parent, String method = "GET", ServerEndpoint endpoint = SUCCESS) { handlerSpan(trace, index, parent, endpoint.name().toLowerCase(), false, endpoint == EXCEPTION, EXCEPTION.body) } void handlerSpan(TraceAssert trace, int index, Object parent, String methodName, boolean isCancelled, boolean isError, String exceptionMessage = null) { trace.span(index) { name "JaxRsTestResource.${methodName}" kind INTERNAL if (isError) { status ERROR errorEvent(Exception, exceptionMessage) } childOf((SpanData) parent) attributes { "$SemanticAttributes.CODE_NAMESPACE" "test.JaxRsTestResource" "$SemanticAttributes.CODE_FUNCTION" methodName if (isCancelled) { "jaxrs.canceled" true } } } } }