/* * Copyright 2016-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.glowroot.agent.plugin.play; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nullable; import org.glowroot.agent.plugin.api.Agent; import org.glowroot.agent.plugin.api.MessageSupplier; import org.glowroot.agent.plugin.api.ThreadContext; import org.glowroot.agent.plugin.api.ThreadContext.Priority; import org.glowroot.agent.plugin.api.TimerName; import org.glowroot.agent.plugin.api.TraceEntry; import org.glowroot.agent.plugin.api.config.BooleanProperty; import org.glowroot.agent.plugin.api.weaving.BindClassMeta; import org.glowroot.agent.plugin.api.weaving.BindParameter; import org.glowroot.agent.plugin.api.weaving.BindReceiver; import org.glowroot.agent.plugin.api.weaving.BindThrowable; import org.glowroot.agent.plugin.api.weaving.BindTraveler; import org.glowroot.agent.plugin.api.weaving.OnBefore; import org.glowroot.agent.plugin.api.weaving.OnReturn; import org.glowroot.agent.plugin.api.weaving.OnThrow; import org.glowroot.agent.plugin.api.weaving.Pointcut; import org.glowroot.agent.plugin.api.weaving.Shim; public class Play2xAspect { private static final Pattern routePattern = Pattern.compile("\\$[^<]+<([^>]+)>"); private static final BooleanProperty useAltTransactionNaming = Agent.getConfigService("play").getBooleanProperty("useAltTransactionNaming"); private static final ConcurrentMap<String, String> simplifiedRoutes = new ConcurrentHashMap<String, String>(); // "play.core.routing.TaggingInvoker" is for play 2.4.x and later // "play.core.Router$Routes$TaggingInvoker" is for play 2.3.x @Shim("play.core.routing.TaggingInvoker|play.core.Router$Routes$TaggingInvoker") public interface TaggingInvoker { @Shim("scala.collection.immutable.Map cachedHandlerTags()") @Nullable ScalaMap glowroot$cachedHandlerTags(); } @Shim("scala.collection.immutable.Map") public interface ScalaMap { @Shim("scala.Option get(java.lang.Object)") @Nullable ScalaOption glowroot$get(Object key); } @Shim("scala.Option") public interface ScalaOption { boolean isDefined(); Object get(); } @Pointcut(className = "play.core.routing.TaggingInvoker|play.core.Router$Routes$TaggingInvoker", methodName = "call", methodParameterTypes = {"scala.Function0"}) public static class HandlerInvokerAdvice { @OnBefore public static void onBefore(ThreadContext context, @BindReceiver TaggingInvoker taggingInvoker) { ScalaMap tags = taggingInvoker.glowroot$cachedHandlerTags(); if (tags == null) { return; } if (useAltTransactionNaming.value()) { ScalaOption controllerOption = tags.glowroot$get("ROUTE_CONTROLLER"); ScalaOption methodOption = tags.glowroot$get("ROUTE_ACTION_METHOD"); if (controllerOption != null && controllerOption.isDefined() && methodOption != null && methodOption.isDefined()) { String controller = controllerOption.get().toString(); String transactionName = getAltTransactionName(controller, methodOption.get().toString()); context.setTransactionName(transactionName, Priority.CORE_PLUGIN); } } else { ScalaOption option = tags.glowroot$get("ROUTE_PATTERN"); if (option != null && option.isDefined()) { String route = option.get().toString(); route = simplifiedRoute(route); context.setTransactionName(route, Priority.CORE_PLUGIN); } } } } @Pointcut(className = "views.html.*", methodName = "apply", methodParameterTypes = {".."}, timerName = "play render", nestingGroup = "play-render") public static class RenderAdvice { private static final TimerName timerName = Agent.getTimerName(RenderAdvice.class); @OnBefore public static TraceEntry onBefore(ThreadContext context, @BindReceiver Object view) { String viewName = view.getClass().getSimpleName(); // strip off trailing $ viewName = viewName.substring(0, viewName.length() - 1); return context.startTraceEntry(MessageSupplier.create("play render: {}", viewName), timerName); } @OnReturn public static void onReturn(@BindTraveler TraceEntry traceEntry) { traceEntry.end(); } @OnThrow public static void onThrow(@BindThrowable Throwable t, @BindTraveler TraceEntry traceEntry) { traceEntry.endWithError(t); } } // visible for testing static String simplifiedRoute(String route) { String simplifiedRoute = simplifiedRoutes.get(route); if (simplifiedRoute == null) { Matcher matcher = routePattern.matcher(route); StringBuilder sb = new StringBuilder(); int end = 0; while (matcher.find()) { if (end == 0) { sb.append(route.substring(0, matcher.start())); } String regex = nullToEmpty(matcher.group(1)); regex = regex.replace("[^/]+", "*"); regex = regex.replace(".+", "**"); sb.append(regex); end = matcher.end(); } sb.append(route.substring(end)); simplifiedRoute = sb.toString(); simplifiedRoutes.putIfAbsent(route, simplifiedRoute); } return simplifiedRoute; } private static String getAltTransactionName(String controller, String methodName) { int index = controller.lastIndexOf('.'); if (index == -1) { return controller + "#" + methodName; } else { return controller.substring(index + 1) + "#" + methodName; } } private static String nullToEmpty(@Nullable String s) { return s == null ? "" : s; } // ========== play 2.0.x - 2.2.x ========== @Shim("play.core.Router$HandlerDef") public interface HandlerDef { @Nullable String controller(); @Nullable String method(); } @Pointcut(className = "play.core.Router$HandlerInvoker", methodName = "call", methodParameterTypes = {"scala.Function0", "play.core.Router$HandlerDef"}) public static class OldHandlerInvokerAdvice { @OnBefore public static void onBefore(ThreadContext context, @SuppressWarnings("unused") @BindParameter Object action, @BindParameter HandlerDef handlerDef, @BindClassMeta PlayInvoker invoker) { String controller = handlerDef.controller(); String method = handlerDef.method(); // path() method doesn't exist in play 2.0.x so need to use reflection instead of shim String path = invoker.path(handlerDef); if (useAltTransactionNaming.value() || path == null) { if (controller != null && method != null) { context.setTransactionName(getAltTransactionName(controller, method), Priority.CORE_PLUGIN); } } else { path = simplifiedRoute(path); context.setTransactionName(path, Priority.CORE_PLUGIN); } } } }