From 02ca471577cfded5fbed0e3a50b347c0c9d7aa8a Mon Sep 17 00:00:00 2001 From: Lauri Tulmin Date: Wed, 3 Feb 2021 17:20:49 +0200 Subject: [PATCH] Wicket instrumentation (#2139) * Wicket instrumentation * Change supported version to 8.0, turns out earlier versions didn't work --- README.md | 1 + ...DefaultExceptionMapperInstrumentation.java | 55 +++++++++++ ...RequestHandlerExecutorInstrumentation.java | 60 ++++++++++++ .../wicket/WicketInstrumentationModule.java | 35 +++++++ .../src/test/groovy/WicketTest.groovy | 97 +++++++++++++++++++ .../test/groovy/hello/ExceptionPage.groovy | 14 +++ .../test/groovy/hello/HelloApplication.groovy | 29 ++++++ .../src/test/groovy/hello/HelloPage.groovy | 15 +++ .../src/test/resources/hello/HelloPage.html | 6 ++ .../javaagent/wicket-8.0-javaagent.gradle | 21 ++++ settings.gradle | 1 + 11 files changed, 334 insertions(+) create mode 100644 instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/DefaultExceptionMapperInstrumentation.java create mode 100644 instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/RequestHandlerExecutorInstrumentation.java create mode 100644 instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/WicketInstrumentationModule.java create mode 100644 instrumentation/wicket-8.0/javaagent/src/test/groovy/WicketTest.groovy create mode 100644 instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/ExceptionPage.groovy create mode 100644 instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/HelloApplication.groovy create mode 100644 instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/HelloPage.groovy create mode 100644 instrumentation/wicket-8.0/javaagent/src/test/resources/hello/HelloPage.html create mode 100644 instrumentation/wicket-8.0/javaagent/wicket-8.0-javaagent.gradle diff --git a/README.md b/README.md index 587510f040..1fadc51693 100644 --- a/README.md +++ b/README.md @@ -318,6 +318,7 @@ These are the supported libraries and frameworks: | [Akka HTTP](https://doc.akka.io/docs/akka-http/current/index.html) | 10.0+ | | [Apache HttpAsyncClient](https://hc.apache.org/index.html) | 4.1+ | | [Apache HttpClient](https://hc.apache.org/index.html) | 2.0+ | +| [Apache Wicket](https://wicket.apache.org/) | 8.0+ | | [Armeria](https://armeria.dev) | 1.3+ | | [AsyncHttpClient](https://github.com/AsyncHttpClient/async-http-client) | 1.9+ (not including 2.x yet) | | [AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/java-handler.html) | 1.0+ | diff --git a/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/DefaultExceptionMapperInstrumentation.java b/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/DefaultExceptionMapperInstrumentation.java new file mode 100644 index 0000000000..b94bd6a47e --- /dev/null +++ b/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/DefaultExceptionMapperInstrumentation.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.wicket; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.javaagent.tooling.TypeInstrumentation; +import java.lang.reflect.InvocationTargetException; +import java.util.Collections; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.wicket.WicketRuntimeException; + +public class DefaultExceptionMapperInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.wicket.DefaultExceptionMapper"); + } + + @Override + public Map, String> transformers() { + return Collections.singletonMap( + named("mapUnexpectedExceptions").and(takesArgument(0, named(Exception.class.getName()))), + DefaultExceptionMapperInstrumentation.class.getName() + "$ExceptionAdvice"); + } + + public static class ExceptionAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onExit(@Advice.Argument(0) Exception exception) { + Span serverSpan = BaseTracer.getCurrentServerSpan(); + if (serverSpan != null) { + // unwrap exception + Throwable throwable = exception; + while (throwable.getCause() != null + && (throwable instanceof WicketRuntimeException + || throwable instanceof InvocationTargetException)) { + throwable = throwable.getCause(); + } + // as we don't create a span for wicket we record exception on server span + serverSpan.recordException(throwable); + } + } + } +} diff --git a/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/RequestHandlerExecutorInstrumentation.java b/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/RequestHandlerExecutorInstrumentation.java new file mode 100644 index 0000000000..d1ed497627 --- /dev/null +++ b/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/RequestHandlerExecutorInstrumentation.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.wicket; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.instrumentation.api.servlet.ServletContextPath; +import io.opentelemetry.instrumentation.api.tracer.BaseTracer; +import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; +import io.opentelemetry.javaagent.tooling.TypeInstrumentation; +import java.util.Collections; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.wicket.core.request.handler.IPageClassRequestHandler; +import org.apache.wicket.request.IRequestHandler; +import org.apache.wicket.request.cycle.RequestCycle; + +public class RequestHandlerExecutorInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.wicket.request.RequestHandlerExecutor"); + } + + @Override + public Map, String> transformers() { + return Collections.singletonMap( + named("execute").and(takesArgument(0, named("org.apache.wicket.request.IRequestHandler"))), + RequestHandlerExecutorInstrumentation.class.getName() + "$ExecuteAdvice"); + } + + public static class ExecuteAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onExit(@Advice.Argument(0) IRequestHandler handler) { + Span serverSpan = BaseTracer.getCurrentServerSpan(); + if (serverSpan == null) { + return; + } + if (handler instanceof IPageClassRequestHandler) { + // using class name as page name + String pageName = ((IPageClassRequestHandler) handler).getPageClass().getName(); + // wicket filter mapping without wildcard, if wicket filter is mapped to /* + // this will be an empty string + String filterPath = RequestCycle.get().getRequest().getFilterPath(); + serverSpan.updateName( + ServletContextPath.prepend( + Java8BytecodeBridge.currentContext(), filterPath + "/" + pageName)); + } + } + } +} diff --git a/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/WicketInstrumentationModule.java b/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/WicketInstrumentationModule.java new file mode 100644 index 0000000000..bc5634d59c --- /dev/null +++ b/instrumentation/wicket-8.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/wicket/WicketInstrumentationModule.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.wicket; + +import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.ClassLoaderMatcher.hasClassesNamed; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.tooling.InstrumentationModule; +import io.opentelemetry.javaagent.tooling.TypeInstrumentation; +import java.util.Arrays; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class WicketInstrumentationModule extends InstrumentationModule { + + public WicketInstrumentationModule() { + super("wicket", "wicket-8.0"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // missing before 8.0 + return hasClassesNamed("org.apache.wicket.request.RequestHandlerExecutor"); + } + + @Override + public List typeInstrumentations() { + return Arrays.asList( + new RequestHandlerExecutorInstrumentation(), new DefaultExceptionMapperInstrumentation()); + } +} diff --git a/instrumentation/wicket-8.0/javaagent/src/test/groovy/WicketTest.groovy b/instrumentation/wicket-8.0/javaagent/src/test/groovy/WicketTest.groovy new file mode 100644 index 0000000000..a9c6c98ede --- /dev/null +++ b/instrumentation/wicket-8.0/javaagent/src/test/groovy/WicketTest.groovy @@ -0,0 +1,97 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan + +import hello.HelloApplication +import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification +import io.opentelemetry.instrumentation.test.base.HttpServerTestTrait +import javax.servlet.DispatcherType +import okhttp3.HttpUrl +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.Response +import org.apache.wicket.protocol.http.WicketFilter +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.servlet.DefaultServlet +import org.eclipse.jetty.servlet.ServletContextHandler +import org.eclipse.jetty.util.resource.FileResource +import org.jsoup.Jsoup + +class WicketTest extends AgentInstrumentationSpecification implements HttpServerTestTrait { + + @Override + Server startServer(int port) { + def server = new Server(port) + ServletContextHandler context = new ServletContextHandler(0) + context.setContextPath(getContextPath()) + def resource = new FileResource(getClass().getResource("/")) + context.setBaseResource(resource) + server.setHandler(context) + + context.addServlet(DefaultServlet, "/") + def registration = context.getServletContext().addFilter("WicketApplication", WicketFilter) + registration.setInitParameter("applicationClassName", HelloApplication.getName()) + registration.setInitParameter("filterMappingUrlPattern", "/wicket-test/*") + registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/wicket-test/*") + + server.start() + + return server + } + + @Override + void stopServer(Server server) { + server.stop() + server.destroy() + } + + @Override + String getContextPath() { + return "/jetty-context" + } + + def "test hello"() { + setup: + def url = HttpUrl.get(address.resolve("wicket-test/")).newBuilder().build() + def request = request(url, "GET", null).build() + Response response = client.newCall(request).execute() + def doc = Jsoup.parse(response.body().string()) + + expect: + response.code() == 200 + doc.selectFirst("#message").text() == "Hello World!" + + assertTraces(1) { + trace(0, 1) { + basicSpan(it, 0, getContextPath() + "/wicket-test/hello.HelloPage") + } + } + } + + def "test exception"() { + setup: + def url = HttpUrl.get(address.resolve("wicket-test/exception")).newBuilder().build() + def request = request(url, "GET", null).build() + Response response = client.newCall(request).execute() + + expect: + response.code() == 500 + + assertTraces(1) { + trace(0, 1) { + basicSpan(it, 0, getContextPath() + "/wicket-test/org.apache.wicket.markup.html.pages.InternalErrorPage", null, new Exception("test exception")) + } + } + } + + Request.Builder request(HttpUrl url, String method, RequestBody body) { + return new Request.Builder() + .url(url) + .method(method, body) + .header("User-Agent", TEST_USER_AGENT) + .header("X-Forwarded-For", TEST_CLIENT_IP) + } +} diff --git a/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/ExceptionPage.groovy b/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/ExceptionPage.groovy new file mode 100644 index 0000000000..2314d98048 --- /dev/null +++ b/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/ExceptionPage.groovy @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package hello + +import org.apache.wicket.markup.html.WebPage + +class ExceptionPage extends WebPage { + ExceptionPage() { + throw new Exception("test exception") + } +} diff --git a/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/HelloApplication.groovy b/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/HelloApplication.groovy new file mode 100644 index 0000000000..6f72950276 --- /dev/null +++ b/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/HelloApplication.groovy @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package hello + +import org.apache.wicket.Page +import org.apache.wicket.RuntimeConfigurationType +import org.apache.wicket.protocol.http.WebApplication + +class HelloApplication extends WebApplication { + @Override + Class getHomePage() { + HelloPage + } + + @Override + protected void init() { + super.init() + + mountPage("/exception", ExceptionPage) + } + + @Override + RuntimeConfigurationType getConfigurationType() { + return RuntimeConfigurationType.DEPLOYMENT + } +} diff --git a/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/HelloPage.groovy b/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/HelloPage.groovy new file mode 100644 index 0000000000..80e0df316e --- /dev/null +++ b/instrumentation/wicket-8.0/javaagent/src/test/groovy/hello/HelloPage.groovy @@ -0,0 +1,15 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package hello + +import org.apache.wicket.markup.html.WebPage +import org.apache.wicket.markup.html.basic.Label + +class HelloPage extends WebPage { + HelloPage() { + add(new Label("message", "Hello World!")) + } +} diff --git a/instrumentation/wicket-8.0/javaagent/src/test/resources/hello/HelloPage.html b/instrumentation/wicket-8.0/javaagent/src/test/resources/hello/HelloPage.html new file mode 100644 index 0000000000..b4cd0e1be9 --- /dev/null +++ b/instrumentation/wicket-8.0/javaagent/src/test/resources/hello/HelloPage.html @@ -0,0 +1,6 @@ + + + + Message goes here + + diff --git a/instrumentation/wicket-8.0/javaagent/wicket-8.0-javaagent.gradle b/instrumentation/wicket-8.0/javaagent/wicket-8.0-javaagent.gradle new file mode 100644 index 0000000000..eda7aa3eea --- /dev/null +++ b/instrumentation/wicket-8.0/javaagent/wicket-8.0-javaagent.gradle @@ -0,0 +1,21 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = 'org.apache.wicket' + module = 'wicket' + versions = "[8.0.0,]" + assertInverse = true + } +} + +dependencies { + library group: 'org.apache.wicket', name: 'wicket', version: '8.0.0' + + testImplementation(project(':testing-common')) + testImplementation group: 'org.jsoup', name: 'jsoup', version: '1.13.1' + testImplementation group: 'org.eclipse.jetty', name: 'jetty-server', version: '8.0.0.v20110901' + testImplementation group: 'org.eclipse.jetty', name: 'jetty-servlet', version: '8.0.0.v20110901' + + testInstrumentation project(":instrumentation:servlet:servlet-3.0:javaagent") +} diff --git a/settings.gradle b/settings.gradle index e16c8b8e2c..ac19adef13 100644 --- a/settings.gradle +++ b/settings.gradle @@ -213,6 +213,7 @@ include ':instrumentation:twilio-6.6:javaagent' include ':instrumentation:undertow:javaagent' include ':instrumentation:vertx-web-3.0' include ':instrumentation:vertx-reactive-3.5:javaagent' +include ':instrumentation:wicket-8.0:javaagent' include ':instrumentation-core:reactor-3.1' include ':instrumentation-core:servlet-2.2'