Skip to content

Commit

Permalink
Add support for vaadin web framework (#2619)
Browse files Browse the repository at this point in the history
* Add support for vaadin

* rename test

* review fixes

* rename module

* review fixes

* Trigger Build

* don't load advice classes
  • Loading branch information
laurit authored Mar 29, 2021
1 parent f856452 commit b999e8a
Show file tree
Hide file tree
Showing 17 changed files with 1,023 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/supported-libraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ These are the supported libraries and frameworks:
| [Struts2](https://github.com/apache/struts) | 2.3+ |
| [Twilio](https://github.com/twilio/twilio-java) | 6.6+ (not including 8.x yet) |
| [Undertow](https://undertow.io/) | 1.4+ |
| [Vaadin](https://vaadin.com/) | 14.2+ |
| [Vert.x](https://vertx.io) | 3.0+ |
| [Vert.x RxJava2](https://vertx.io/docs/vertx-rx/java2/) | 3.5+ |

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.vaadin;

import static io.opentelemetry.javaagent.instrumentation.vaadin.VaadinTracer.tracer;
import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.implementsInterface;
import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.ClassLoaderMatcher.hasClassesNamed;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;

import com.google.auto.service.AutoService;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.router.Location;
import com.vaadin.flow.router.NavigationTrigger;
import com.vaadin.flow.server.RequestHandler;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.communication.rpc.RpcInvocationHandler;
import elemental.json.JsonObject;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.lang.reflect.Method;
import java.util.List;
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;

@AutoService(InstrumentationModule.class)
public class VaadinInstrumentationModule extends InstrumentationModule {

public VaadinInstrumentationModule() {
super("vaadin", "vaadin-14.2");
}

@Override
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
// class added in vaadin 14.2
return hasClassesNamed("com.vaadin.flow.server.frontend.installer.NodeInstaller");
}

@Override
public List<TypeInstrumentation> typeInstrumentations() {
return asList(
new VaadinServiceInstrumentation(),
new RequestHandlerInstrumentation(),
new UiInstrumentation(),
new RouterInstrumentation(),
new JavaScriptBootstrapUiInstrumentation(),
new RpcInvocationHandlerInstrumentation(),
new ClientCallableRpcInstrumentation());
}

// add span around vaadin request processing code
public static class VaadinServiceInstrumentation implements TypeInstrumentation {

@Override
public ElementMatcher<? super TypeDescription> typeMatcher() {
return named("com.vaadin.flow.server.VaadinService");
}

@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
named("handleRequest")
.and(takesArgument(0, named("com.vaadin.flow.server.VaadinRequest")))
.and(takesArgument(1, named("com.vaadin.flow.server.VaadinResponse"))),
VaadinServiceInstrumentation.class.getName() + "$HandleRequestAdvice");
}

public static class HandleRequestAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(
@Advice.This VaadinService vaadinService,
@Advice.Origin Method method,
@Advice.Local("otelContext") Context context,
@Advice.Local("otelScope") Scope scope) {
context = tracer().startVaadinServiceSpan(vaadinService, method);
scope = context.makeCurrent();
}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void onExit(
@Advice.Thrown Throwable throwable,
@Advice.Local("otelContext") Context context,
@Advice.Local("otelScope") Scope scope) {
scope.close();

tracer().endVaadinServiceSpan(context, throwable);
}
}
}

// add spans around vaadin request handlers
public static class RequestHandlerInstrumentation implements TypeInstrumentation {

@Override
public ElementMatcher<ClassLoader> classLoaderOptimization() {
return hasClassesNamed("com.vaadin.flow.server.RequestHandler");
}

@Override
public ElementMatcher<? super TypeDescription> typeMatcher() {
return implementsInterface(named("com.vaadin.flow.server.RequestHandler"));
}

@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
named("handleRequest")
.and(takesArgument(0, named("com.vaadin.flow.server.VaadinSession")))
.and(takesArgument(1, named("com.vaadin.flow.server.VaadinRequest")))
.and(takesArgument(2, named("com.vaadin.flow.server.VaadinResponse"))),
RequestHandlerInstrumentation.class.getName() + "$RequestHandlerAdvice");
}

public static class RequestHandlerAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(
@Advice.This RequestHandler requestHandler,
@Advice.Origin Method method,
@Advice.Local("otelContext") Context context,
@Advice.Local("otelScope") Scope scope) {

context = tracer().startRequestHandlerSpan(requestHandler, method);
if (context != null) {
scope = context.makeCurrent();
}
}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void onExit(
@Advice.Thrown Throwable throwable,
@Advice.Return boolean handled,
@Advice.Local("otelContext") Context context,
@Advice.Local("otelScope") Scope scope) {
if (scope == null) {
return;
}
scope.close();

tracer().endRequestHandlerSpan(context, throwable, handled);
}
}
}

// update server span name to route of current view
public static class UiInstrumentation implements TypeInstrumentation {

@Override
public ElementMatcher<? super TypeDescription> typeMatcher() {
return named("com.vaadin.flow.component.UI");
}

@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
// setCurrent is called by some request handler when they have accepted the request
// we can get the path of currently active route from ui
return singletonMap(
named("setCurrent").and(takesArgument(0, named("com.vaadin.flow.component.UI"))),
UiInstrumentation.class.getName() + "$SetUiAdvice");
}

public static class SetUiAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(@Advice.Argument(0) UI ui) {
tracer().updateServerSpanName(ui);
}
}
}

// set server span name on initial page load
public static class RouterInstrumentation implements TypeInstrumentation {

@Override
public ElementMatcher<? super TypeDescription> typeMatcher() {
return named("com.vaadin.flow.router.Router");
}

@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
named("navigate")
.and(takesArguments(4))
.and(takesArgument(1, named("com.vaadin.flow.router.Location")))
.and(takesArgument(2, named("com.vaadin.flow.router.NavigationTrigger"))),
RouterInstrumentation.class.getName() + "$NavigateAdvice");
}

public static class NavigateAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(
@Advice.Argument(1) Location location,
@Advice.Argument(2) NavigationTrigger navigationTrigger) {
if (navigationTrigger == NavigationTrigger.PAGE_LOAD) {
tracer().updateServerSpanName(location);
}
}
}
}

// set server span name on initial page load, vaadin 15+
public static class JavaScriptBootstrapUiInstrumentation implements TypeInstrumentation {

@Override
public ElementMatcher<? super TypeDescription> typeMatcher() {
return named("com.vaadin.flow.component.internal.JavaScriptBootstrapUI");
}

@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
named("connectClient"),
JavaScriptBootstrapUiInstrumentation.class.getName() + "$ConnectViewAdvice");
}

public static class ConnectViewAdvice {
@Advice.OnMethodExit(suppress = Throwable.class)
public static void onExit(@Advice.This UI ui) {
tracer().updateServerSpanName(ui);
}
}
}

// add span around rpc calls from javascript
public static class RpcInvocationHandlerInstrumentation implements TypeInstrumentation {

@Override
public ElementMatcher<ClassLoader> classLoaderOptimization() {
return hasClassesNamed("com.vaadin.flow.server.communication.rpc.RpcInvocationHandler");
}

@Override
public ElementMatcher<? super TypeDescription> typeMatcher() {
return implementsInterface(
named("com.vaadin.flow.server.communication.rpc.RpcInvocationHandler"));
}

@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
named("handle")
.and(takesArgument(0, named("com.vaadin.flow.component.UI")))
.and(takesArgument(1, named("elemental.json.JsonObject"))),
RpcInvocationHandlerInstrumentation.class.getName() + "$RpcInvocationHandlerAdvice");
}

public static class RpcInvocationHandlerAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(
@Advice.This RpcInvocationHandler rpcInvocationHandler,
@Advice.Origin Method method,
@Advice.Argument(1) JsonObject jsonObject,
@Advice.Local("otelContext") Context context,
@Advice.Local("otelScope") Scope scope) {

context = tracer().startRpcInvocationHandlerSpan(rpcInvocationHandler, method, jsonObject);
scope = context.makeCurrent();
}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void onExit(
@Advice.Thrown Throwable throwable,
@Advice.Local("otelContext") Context context,
@Advice.Local("otelScope") Scope scope) {
scope.close();

tracer().endSpan(context, throwable);
}
}
}

// add spans around calls to methods with @ClientCallable annotation
public static class ClientCallableRpcInstrumentation implements TypeInstrumentation {

@Override
public ElementMatcher<? super TypeDescription> typeMatcher() {
return named(
"com.vaadin.flow.server.communication.rpc.PublishedServerEventHandlerRpcHandler");
}

@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
named("invokeMethod")
.and(takesArgument(0, named("com.vaadin.flow.component.Component")))
.and(takesArgument(1, named(Class.class.getName())))
.and(takesArgument(2, named(String.class.getName())))
.and(takesArgument(3, named("elemental.json.JsonArray")))
.and(takesArgument(4, named(int.class.getName()))),
ClientCallableRpcInstrumentation.class.getName() + "$InvokeAdvice");
}

public static class InvokeAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(
@Advice.Argument(1) Class<?> componentClass,
@Advice.Argument(2) String methodName,
@Advice.Local("otelContext") Context context,
@Advice.Local("otelScope") Scope scope) {

context = tracer().startClientCallableSpan(componentClass, methodName);
scope = context.makeCurrent();
}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void onExit(
@Advice.Thrown Throwable throwable,
@Advice.Local("otelContext") Context context,
@Advice.Local("otelScope") Scope scope) {
scope.close();

tracer().endSpan(context, throwable);
}
}
}
}
Loading

0 comments on commit b999e8a

Please sign in to comment.