diff --git a/instrumentation/jetty/jetty-8.0/javaagent/build.gradle.kts b/instrumentation/jetty/jetty-8.0/javaagent/build.gradle.kts index bf483a0b2db7..dbdb36ea17c0 100644 --- a/instrumentation/jetty/jetty-8.0/javaagent/build.gradle.kts +++ b/instrumentation/jetty/jetty-8.0/javaagent/build.gradle.kts @@ -29,3 +29,11 @@ dependencies { latestDepTestLibrary("org.eclipse.jetty:jetty-server:10.+") // see jetty-11.0 module latestDepTestLibrary("org.eclipse.jetty:jetty-servlet:10.+") // see jetty-11.0 module } + +// jetty-server 10+ requires Java 11 +val latestDepTest = findProperty("testLatestDeps") as Boolean +if (latestDepTest) { + otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_11) + } +} diff --git a/instrumentation/jetty/jetty-8.0/javaagent/src/test/groovy/JettyHandlerTest.groovy b/instrumentation/jetty/jetty-8.0/javaagent/src/test/groovy/JettyHandlerTest.groovy deleted file mode 100644 index 37b1dc0bf24b..000000000000 --- a/instrumentation/jetty/jetty-8.0/javaagent/src/test/groovy/JettyHandlerTest.groovy +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.api.common.AttributeKey -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.semconv.trace.attributes.SemanticAttributes -import org.eclipse.jetty.server.Request -import org.eclipse.jetty.server.Response -import org.eclipse.jetty.server.Server -import org.eclipse.jetty.server.handler.AbstractHandler -import org.eclipse.jetty.server.handler.ErrorHandler -import spock.lang.Shared - -import javax.servlet.DispatcherType -import javax.servlet.ServletException -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse - -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS -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.QUERY_PARAM -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT -import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS - -class JettyHandlerTest extends HttpServerTest implements AgentTestTrait { - - static ErrorHandler errorHandler = new ErrorHandler() { - @Override - protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message) throws IOException { - Throwable th = (Throwable) request.getAttribute("javax.servlet.error.exception") - message = th ? th.message : message - if (message) { - writer.write(message) - } - } - } - - @Shared - TestHandler testHandler = new TestHandler() - - @Override - Server startServer(int port) { - def server = new Server(port) - server.setHandler(handler()) - server.addBean(errorHandler) - server.start() - return server - } - - AbstractHandler handler() { - testHandler - } - - @Override - void stopServer(Server server) { - server.stop() - } - - @Override - Set> httpAttributes(ServerEndpoint endpoint) { - def attributes = super.httpAttributes(endpoint) - attributes.remove(SemanticAttributes.HTTP_ROUTE) - attributes - } - - @Override - boolean hasResponseSpan(ServerEndpoint endpoint) { - endpoint == REDIRECT || endpoint == ERROR - } - - @Override - void responseSpan(TraceAssert trace, int index, Object parent, String method, ServerEndpoint endpoint) { - switch (endpoint) { - case REDIRECT: - redirectSpan(trace, index, parent) - break - case ERROR: - sendErrorSpan(trace, index, parent) - break - } - } - - static void handleRequest(Request request, HttpServletResponse response) { - ServerEndpoint endpoint = ServerEndpoint.forPath(request.requestURI) - controller(endpoint) { - response.contentType = "text/plain" - switch (endpoint) { - case SUCCESS: - response.status = endpoint.status - response.writer.print(endpoint.body) - break - case QUERY_PARAM: - response.status = endpoint.status - response.writer.print(request.queryString) - break - case REDIRECT: - response.sendRedirect(endpoint.body) - break - case ERROR: - response.sendError(endpoint.status, endpoint.body) - break - case CAPTURE_HEADERS: - response.setHeader("X-Test-Response", request.getHeader("X-Test-Request")) - response.status = endpoint.status - response.writer.print(endpoint.body) - break - case EXCEPTION: - throw new Exception(endpoint.body) - case INDEXED_CHILD: - INDEXED_CHILD.collectSpanAttributes { name -> request.getParameter(name) } - response.status = endpoint.status - response.writer.print(endpoint.body) - break - default: - response.status = NOT_FOUND.status - response.writer.print(NOT_FOUND.body) - break - } - } - } - - static class TestHandler extends AbstractHandler { - @Override - void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - //This line here is to verify that we don't break Jetty if it wants to cast to implementation class - //See https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/1096 - Response jettyResponse = response as Response - if (baseRequest.dispatcherType != DispatcherType.ERROR) { - handleRequest(baseRequest, jettyResponse) - baseRequest.handled = true - } else { - errorHandler.handle(target, baseRequest, baseRequest, response) - } - } - } -} diff --git a/instrumentation/jetty/jetty-8.0/javaagent/src/test/groovy/QueuedThreadPoolTest.groovy b/instrumentation/jetty/jetty-8.0/javaagent/src/test/groovy/QueuedThreadPoolTest.groovy deleted file mode 100644 index 020e9cc4bad6..000000000000 --- a/instrumentation/jetty/jetty-8.0/javaagent/src/test/groovy/QueuedThreadPoolTest.groovy +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.api.trace.SpanKind -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import org.eclipse.jetty.util.thread.QueuedThreadPool - -import static org.junit.jupiter.api.Assumptions.assumeTrue - -class QueuedThreadPoolTest extends AgentInstrumentationSpecification { - - def "QueueThreadPool 'dispatch' propagates"() { - setup: - def pool = new QueuedThreadPool() - // run test only if QueuedThreadPool has dispatch method - // dispatch method was removed in jetty 9.1 - assumeTrue(pool.metaClass.getMetaMethod("dispatch", Runnable) != null) - pool.start() - - new Runnable() { - @Override - void run() { - runWithSpan("parent") { - // this child will have a span - def child1 = new JavaAsyncChild() - // this child won't - def child2 = new JavaAsyncChild(false, false) - pool.dispatch(child1) - pool.dispatch(child2) - child1.waitForCompletion() - child2.waitForCompletion() - } - } - }.run() - - expect: - assertTraces(1) { - trace(0, 2) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - } - span(1) { - name "asyncChild" - kind SpanKind.INTERNAL - childOf span(0) - } - } - } - - cleanup: - pool.stop() - } - - def "QueueThreadPool 'dispatch' propagates lambda"() { - setup: - def pool = new QueuedThreadPool() - // run test only if QueuedThreadPool has dispatch method - // dispatch method was removed in jetty 9.1 - assumeTrue(pool.metaClass.getMetaMethod("dispatch", Runnable) != null) - pool.start() - - JavaAsyncChild child = new JavaAsyncChild(true, true) - new Runnable() { - @Override - void run() { - runWithSpan("parent") { - pool.dispatch(JavaLambdaMaker.lambda(child)) - } - } - }.run() - // We block in child to make sure spans close in predictable order - child.unblock() - child.waitForCompletion() - - expect: - assertTraces(1) { - trace(0, 2) { - span(0) { - name "parent" - kind SpanKind.INTERNAL - hasNoParent() - } - span(1) { - name "asyncChild" - kind SpanKind.INTERNAL - childOf span(0) - } - } - } - - cleanup: - pool.stop() - } -} diff --git a/instrumentation/jetty/jetty-8.0/javaagent/src/test/groovy/JavaAsyncChild.java b/instrumentation/jetty/jetty-8.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JavaAsyncChild.java similarity index 96% rename from instrumentation/jetty/jetty-8.0/javaagent/src/test/groovy/JavaAsyncChild.java rename to instrumentation/jetty/jetty-8.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JavaAsyncChild.java index 66cd7c990411..ff98699fe371 100644 --- a/instrumentation/jetty/jetty-8.0/javaagent/src/test/groovy/JavaAsyncChild.java +++ b/instrumentation/jetty/jetty-8.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JavaAsyncChild.java @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.jetty.v8_0; + import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.trace.Tracer; import java.util.concurrent.Callable; diff --git a/instrumentation/jetty/jetty-8.0/javaagent/src/test/java/JavaLambdaMaker.java b/instrumentation/jetty/jetty-8.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JavaLambdaMaker.java similarity index 81% rename from instrumentation/jetty/jetty-8.0/javaagent/src/test/java/JavaLambdaMaker.java rename to instrumentation/jetty/jetty-8.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JavaLambdaMaker.java index c620351ebce7..2f34db1b173d 100644 --- a/instrumentation/jetty/jetty-8.0/javaagent/src/test/java/JavaLambdaMaker.java +++ b/instrumentation/jetty/jetty-8.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JavaLambdaMaker.java @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.javaagent.instrumentation.jetty.v8_0; + public class JavaLambdaMaker { @SuppressWarnings("FunctionalExpressionCanBeFolded") diff --git a/instrumentation/jetty/jetty-8.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JettyHandlerTest.java b/instrumentation/jetty/jetty-8.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JettyHandlerTest.java new file mode 100644 index 000000000000..00f3986c0706 --- /dev/null +++ b/instrumentation/jetty/jetty-8.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/JettyHandlerTest.java @@ -0,0 +1,175 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.v8_0; + +import static io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions.DEFAULT_HTTP_ATTRIBUTES; +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.REDIRECT; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.Sets; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions; +import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.io.IOException; +import java.io.Writer; +import java.util.Collections; +import javax.servlet.DispatcherType; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.server.handler.ErrorHandler; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class JettyHandlerTest extends AbstractHttpServerTest { + + @RegisterExtension + static final InstrumentationExtension testing = HttpServerInstrumentationExtension.forAgent(); + + private static final ErrorHandler errorHandler = + new ErrorHandler() { + @Override + protected void handleErrorPage( + HttpServletRequest request, Writer writer, int code, String message) + throws IOException { + Throwable th = (Throwable) request.getAttribute("javax.servlet.error.exception"); + String errorMsg = th != null ? th.getMessage() : message; + if (errorMsg != null) { + writer.write(errorMsg); + } + } + }; + + private static final TestHandler testHandler = new TestHandler(); + + @Override + protected Server setupServer() { + Server server = new Server(port); + server.setHandler(testHandler); + server.addBean(errorHandler); + try { + server.start(); + } catch (Exception e) { + throw new RuntimeException(e); + } + return server; + } + + @Override + protected void stopServer(Server server) { + try { + server.stop(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + protected void configure(HttpServerTestOptions options) { + options.setHttpAttributes( + unused -> + Sets.difference( + DEFAULT_HTTP_ATTRIBUTES, Collections.singleton(SemanticAttributes.HTTP_ROUTE))); + options.setHasResponseSpan(endpoint -> endpoint == REDIRECT || endpoint == ERROR); + options.setExpectedException(new IllegalStateException(EXCEPTION.getBody())); + } + + @Override + protected SpanDataAssert assertResponseSpan( + SpanDataAssert span, String method, ServerEndpoint endpoint) { + if (endpoint == REDIRECT) { + span.satisfies(spanData -> assertThat(spanData.getName()).endsWith(".sendRedirect")); + } else if (endpoint == ERROR) { + span.satisfies(spanData -> assertThat(spanData.getName()).endsWith(".sendError")); + } + span.hasKind(SpanKind.INTERNAL).hasAttributesSatisfying(Attributes::isEmpty); + return span; + } + + private static void handleRequest(Request request, HttpServletResponse response) { + ServerEndpoint endpoint = ServerEndpoint.forPath(request.getRequestURI()); + controller( + endpoint, + () -> { + try { + return response(request, response, endpoint); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + private static HttpServletResponse response( + Request request, HttpServletResponse response, ServerEndpoint endpoint) throws IOException { + response.setContentType("text/plain"); + switch (endpoint) { + case SUCCESS: + response.setStatus(endpoint.getStatus()); + response.getWriter().print(endpoint.getBody()); + break; + case QUERY_PARAM: + response.setStatus(endpoint.getStatus()); + response.getWriter().print(request.getQueryString()); + break; + case REDIRECT: + response.sendRedirect(endpoint.getBody()); + break; + case ERROR: + response.sendError(endpoint.getStatus(), endpoint.getBody()); + break; + case CAPTURE_HEADERS: + response.setHeader("X-Test-Response", request.getHeader("X-Test-Request")); + response.setStatus(endpoint.getStatus()); + response.getWriter().print(endpoint.getBody()); + break; + case EXCEPTION: + throw new IllegalStateException(endpoint.getBody()); + case INDEXED_CHILD: + INDEXED_CHILD.collectSpanAttributes(name -> request.getParameter(name)); + response.setStatus(endpoint.getStatus()); + response.getWriter().print(endpoint.getBody()); + break; + default: + response.setStatus(NOT_FOUND.getStatus()); + response.getWriter().print(NOT_FOUND.getBody()); + break; + } + return response; + } + + private static class TestHandler extends AbstractHandler { + @Override + public void handle( + String target, + Request baseRequest, + HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException { + // This line here is to verify that we don't break Jetty if it wants to cast to implementation + // class + Response jettyResponse = (Response) response; + if (baseRequest.getDispatcherType() != DispatcherType.ERROR) { + handleRequest(baseRequest, jettyResponse); + baseRequest.setHandled(true); + } else { + errorHandler.handle(target, baseRequest, baseRequest, response); + } + } + } +} diff --git a/instrumentation/jetty/jetty-8.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/QueuedThreadPoolTest.java b/instrumentation/jetty/jetty-8.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/QueuedThreadPoolTest.java new file mode 100644 index 000000000000..8577fae40bc6 --- /dev/null +++ b/instrumentation/jetty/jetty-8.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jetty/v8_0/QueuedThreadPoolTest.java @@ -0,0 +1,104 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jetty.v8_0; + +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.lang.reflect.Method; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class QueuedThreadPoolTest { + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void dispatchPropagates() throws Exception { + QueuedThreadPool pool = new QueuedThreadPool(); + // run test only if QueuedThreadPool has dispatch method + // dispatch method was removed in jetty 9.1 + Method dispatch = null; + try { + dispatch = QueuedThreadPool.class.getMethod("dispatch", Runnable.class); + } catch (NoSuchMethodException ignore) { + // ignore + } + assumeTrue(dispatch != null); + pool.start(); + + Method finalDispatch = dispatch; + testing.runWithSpan( + "parent", + () -> { + // this child will have a span + JavaAsyncChild child1 = new JavaAsyncChild(); + // this child won't + JavaAsyncChild child2 = new JavaAsyncChild(false, false); + if (finalDispatch != null) { + finalDispatch.invoke(pool, child1); + finalDispatch.invoke(pool, child2); + child1.waitForCompletion(); + child2.waitForCompletion(); + } + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactlyInAnyOrder( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("asyncChild") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + + pool.stop(); + } + + @Test + void dispatchPropagatesLambda() throws Exception { + QueuedThreadPool pool = new QueuedThreadPool(); + // run test only if QueuedThreadPool has dispatch method + // dispatch method was removed in jetty 9.1 + Method dispatch = null; + try { + dispatch = QueuedThreadPool.class.getMethod("dispatch", Runnable.class); + } catch (NoSuchMethodException ignore) { + // ignore + } + assumeTrue(dispatch != null); + pool.start(); + + JavaAsyncChild child = new JavaAsyncChild(true, true); + Method finalDispatch = dispatch; + testing.runWithSpan( + "parent", + () -> { + if (finalDispatch != null) { + finalDispatch.invoke(pool, JavaLambdaMaker.lambda(child)); + } + }); + + // We block in child to make sure spans close in predictable order + child.unblock(); + child.waitForCompletion(); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactlyInAnyOrder( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("asyncChild") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + + pool.stop(); + } +}