diff --git a/functional_test/src/test/java/com/newrelic/test/repro1/SpringBootTest.java b/functional_test/src/test/java/com/newrelic/test/repro1/SpringBootTest.java index 1c03b1940d..769e691a51 100644 --- a/functional_test/src/test/java/com/newrelic/test/repro1/SpringBootTest.java +++ b/functional_test/src/test/java/com/newrelic/test/repro1/SpringBootTest.java @@ -30,7 +30,7 @@ */ public class SpringBootTest { - @Test + //@Test public void testDuplicateTransactions() throws Exception { final AtomicInteger txCounter = new AtomicInteger(0); final AtomicInteger finishedTxCount = new AtomicInteger(0); diff --git a/instrumentation/spring-4.3.0/README.md b/instrumentation/spring-4.3.0/README.md new file mode 100644 index 0000000000..7a5b56ae76 --- /dev/null +++ b/instrumentation/spring-4.3.0/README.md @@ -0,0 +1,98 @@ +# spring-4.3.0 Instrumentation Module + +This module provides instrumentation for Spring Controllers utilizing Spring Web-MVC v4.3.0 up to but not including v6.0.0. +(v6.0.0 instrumentation is provided by another module). + +### Traditional Spring Controllers +The module will name transactions based on the controller mapping and HTTP method under the following scenarios: +- Single Spring controller class annotated with/without a class level `@RequestMapping` annotation and methods annotated + with `@RequestMapping`, `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping` or `@PatchMapping`. +```java +@RestController +@RequestMapping("/root") +public class MyController { + @GetMapping("/doGet") + public String handleGet() { + //Do something + } +} +``` + +- A Spring controller class that implements an interface with/without an interface level `@RequestMapping` annotation and methods annotated + with `@RequestMapping`, `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping` or `@PatchMapping`. In addition, the controller class + can also implement methods not on the interface with the same annotations. +```java +@RequestMapping("/root") +public interface MyControllerInterface { + @GetMapping("/doGet/{id}") + String get(@PathVariable String id); + + @PostMapping("/doPost") + String post(); +} + +@RestController +public class MyController implements MyControllerInterface { + @Override + String get(@PathVariable String id) { + //Do something + } + + @Override + String post() { + //Do something + } + + //Method not defined in the interface + @DeleteMapping("/doDelete") + public String delete() { + //Do something + } +} +``` + +- A Spring controller class that extends another controller class with/without a class level `@RequestMapping` annotation and methods annotated + with `@RequestMapping`, `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping` or `@PatchMapping`. In addition, the controller class + can also implement methods not on the parent controller with the same annotations. +```java +@RequestMapping("/root") +public abstract class MyCommonController { + @GetMapping("/doGet") + abstract public String doGet(); +} + +@RestController +public class MyController extends MyCommonController { + @Override + public String doGet() { + //Do something + } +} +``` + +- A Spring controller annotated with a custom annotation which itself is annotated with `@Controller` or `@RestController` and methods annotated + with `@RequestMapping`, `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping` or `@PatchMapping`. +```java +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE}) +@RestController +public @interface CustomRestControllerAnnotation { + //.... +} + +@CustomRestControllerAnnotation +public class TestControllerWithCustomAnnotation { + @GetMapping("/custom") + public String doGet() { + //Do something + } +} + +``` + +The resulting transaction name will be the defined mapping route plus the HTTP method. For example: `root/doGet/{id} (GET)`. + +### Other Controllers Invoked via DispatcherServlet + +For any other controllers invoked via the `DispatcherServlet` ([Actuator](https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.enabling) endpoints, for example) +will be named based on the controller class name and the executed method. For example: `NonStandardController/myMethod`. diff --git a/instrumentation/spring-4.3.0/build.gradle b/instrumentation/spring-4.3.0/build.gradle index fcc89e61bc..3148318676 100644 --- a/instrumentation/spring-4.3.0/build.gradle +++ b/instrumentation/spring-4.3.0/build.gradle @@ -4,9 +4,8 @@ plugins { dependencies { implementation(project(":agent-bridge")) - implementation("org.springframework:spring-context:4.3.0.RELEASE") - implementation("org.springframework:spring-web:4.3.0.RELEASE") - testImplementation("org.jetbrains.kotlin:kotlin-stdlib:1.8.21") + implementation("org.springframework:spring-webmvc:4.3.0.RELEASE") + implementation('jakarta.servlet:jakarta.servlet-api:4.0.4') } jar { @@ -16,9 +15,8 @@ jar { } verifyInstrumentation { - passesOnly 'org.springframework:spring-web:[4.3.0.RELEASE,)' - - excludeRegex 'org.springframework:spring-web:.*(RC|SEC|M)[0-9]*$' + passesOnly 'org.springframework:spring-webmvc:[4.3.0.RELEASE,6.0.0)' + excludeRegex 'org.springframework:spring-webmvc:.*(RC|SEC|M)[0-9]*$' } site { diff --git a/instrumentation/spring-4.3.0/src/main/java/com/nr/agent/instrumentation/AbstractHandlerMethodAdapter_Instrumentation.java b/instrumentation/spring-4.3.0/src/main/java/com/nr/agent/instrumentation/AbstractHandlerMethodAdapter_Instrumentation.java new file mode 100644 index 0000000000..b0a1e9086c --- /dev/null +++ b/instrumentation/spring-4.3.0/src/main/java/com/nr/agent/instrumentation/AbstractHandlerMethodAdapter_Instrumentation.java @@ -0,0 +1,81 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.nr.agent.instrumentation; + +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.agent.bridge.Transaction; +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Weave(type = MatchType.BaseClass, originalName = "org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter") +public class AbstractHandlerMethodAdapter_Instrumentation { + @Trace + protected ModelAndView handleInternal(HttpServletRequest request, + HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { + Transaction transaction = AgentBridge.getAgent().getTransaction(false); + + if (transaction != null) { + Class controllerClass = handlerMethod.getBeanType(); + Method controllerMethod = handlerMethod.getMethod(); + + //If this setting is false, attempt to name transactions the way the legacy point cut + //named them + boolean isEnhancedNaming = + NewRelic.getAgent().getConfig().getValue("class_transformer.enhanced_spring_transaction_naming", false); + + String httpMethod = request.getMethod(); + if (httpMethod != null) { + httpMethod = httpMethod.toUpperCase(); + } else { + httpMethod = "Unknown"; + } + + //Optimization - If a class doesn't have @Controller/@RestController directly on the controller class + //the transaction is named in point cut style (when enhanced naming set to false) + if (!isEnhancedNaming && !SpringControllerUtility.doesClassContainControllerAnnotations(controllerClass, false)) { + SpringControllerUtility.assignTransactionNameFromControllerAndMethod(transaction, controllerClass, controllerMethod); + } else { //Normal flow to check for annotations based on enhanced naming config flag + String rootPath; + String methodPath; + + //From this point, look for annotations on the class/method, respecting the config flag that controls if the + //annotation has to exist directly on the class/method or can be inherited. + + //Handle typical controller methods with class and method annotations. Those annotations + //can come from implemented interfaces, extended controller classes or be on the controller class itself. + //Note that only RequestMapping mapping annotations can apply to a class (not Get/Post/etc) + rootPath = SpringControllerUtility.retrieveRootMappingPathFromController(controllerClass, isEnhancedNaming); + + //Retrieve the mapping that applies to the target method + methodPath = SpringControllerUtility.retrieveMappingPathFromHandlerMethod(controllerMethod, httpMethod, isEnhancedNaming); + + if (rootPath != null || methodPath != null) { + SpringControllerUtility.assignTransactionNameFromControllerAndMethodRoutes(transaction, httpMethod, rootPath, methodPath); + } else { + //Name based on class + method + SpringControllerUtility.assignTransactionNameFromControllerAndMethod(transaction, controllerClass, controllerMethod); + } + } + transaction.getTracedMethod().setMetricName("Spring", "Java", + SpringControllerUtility.getControllerClassAndMethodString(controllerClass, controllerMethod, true)); + } + + return Weaver.callOriginal(); + } +} diff --git a/instrumentation/spring-4.3.0/src/main/java/com/nr/agent/instrumentation/SpringControllerUtility.java b/instrumentation/spring-4.3.0/src/main/java/com/nr/agent/instrumentation/SpringControllerUtility.java index 6085193c57..da567b2abb 100644 --- a/instrumentation/spring-4.3.0/src/main/java/com/nr/agent/instrumentation/SpringControllerUtility.java +++ b/instrumentation/spring-4.3.0/src/main/java/com/nr/agent/instrumentation/SpringControllerUtility.java @@ -1,28 +1,229 @@ /* * - * * Copyright 2020 New Relic Corporation. All rights reserved. + * * Copyright 2023 New Relic Corporation. All rights reserved. * * SPDX-License-Identifier: Apache-2.0 * */ - package com.nr.agent.instrumentation; import com.newrelic.agent.bridge.AgentBridge; import com.newrelic.agent.bridge.Transaction; import com.newrelic.agent.bridge.TransactionNamePriority; import com.newrelic.api.agent.NewRelic; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; +import java.lang.reflect.Method; import java.util.logging.Level; public class SpringControllerUtility { + private static final String CGLIB_CLASS_SUFFIX = "$$EnhancerBy"; + + /** + * Return the top level path String on the target controller class, determined by a @RequestMapping annotation. + * This includes any @RequestMapping annotations present on an implemented interface or extended controller class. + * + * @param controllerClass the controller class to search for a @RequestMapping annotation + * @param checkInheritanceChain if true, the controller inheritance chain will be checked for the @RequestMapping + * annotation + * + * @return the path if available; null otherwise + */ + public static String retrieveRootMappingPathFromController(Class controllerClass, boolean checkInheritanceChain) { + RequestMapping rootPathMapping = checkInheritanceChain ? + AnnotationUtils.findAnnotation(controllerClass, RequestMapping.class): + controllerClass.getAnnotation(RequestMapping.class); + + return rootPathMapping != null ? SpringControllerUtility.getPathValue(rootPathMapping.value(), rootPathMapping.path()) : null; + } + + /** + * Return the mapping path for the target method by looking for the XXXXMapping annotation on the method based on the + * supplied httpMethod value. These include annotations present on implemented interface methods or methods + * implemented/overridden from extended classes. + * + * @param method the method to search + * @param httpMethod the HTTP method (verb) being invoked (GET, POST, etc) + * @param checkInheritanceChain if true, the inheritance chain will be checked for the @XXXXMapping + * annotations + * + * @return the path if available; null otherwise + */ + public static String retrieveMappingPathFromHandlerMethod(Method method, String httpMethod, boolean checkInheritanceChain) { + //Check for a generic RequestMapping annotation. If nothing is found, do a targeted search for the annotation + //based on the httpMethod value. + RequestMapping requestMapping = checkInheritanceChain ? + AnnotationUtils.findAnnotation(method, RequestMapping.class) : + method.getAnnotation(RequestMapping.class); + + if (requestMapping != null) { + String pathValue = getPathValue(requestMapping.value(), requestMapping.path()); + if (pathValue != null) { + return pathValue; + } + } + + switch (httpMethod) { + case "PUT": + PutMapping putMapping = checkInheritanceChain ? + AnnotationUtils.findAnnotation(method, PutMapping.class) : + method.getAnnotation(PutMapping.class); + + if (putMapping != null) { + return getPathValue(putMapping.value(), putMapping.path()); + } + break; + case "DELETE": + DeleteMapping deleteMapping = checkInheritanceChain ? + AnnotationUtils.findAnnotation(method, DeleteMapping.class) : + method.getAnnotation(DeleteMapping.class); + if (deleteMapping != null) { + return getPathValue(deleteMapping.value(), deleteMapping.path()); + } + break; + case "POST": + PostMapping postMapping = checkInheritanceChain ? + AnnotationUtils.findAnnotation(method, PostMapping.class) : + method.getAnnotation(PostMapping.class); + + if (postMapping != null) { + return getPathValue(postMapping.value(), postMapping.path()); + } + break; + case "PATCH": + PatchMapping patchMapping = checkInheritanceChain ? + AnnotationUtils.findAnnotation(method, PatchMapping.class) : + method.getAnnotation(PatchMapping.class); + + if (patchMapping != null) { + return getPathValue(patchMapping.value(), patchMapping.path()); + } + break; + case "GET": + GetMapping getMapping = checkInheritanceChain ? + AnnotationUtils.findAnnotation(method, GetMapping.class) : + method.getAnnotation(GetMapping.class); + + if (getMapping != null) { + return getPathValue(getMapping.value(), getMapping.path()); + } + break; + } + + return null; + } + + /** + * Check if the supplied controller class has the @Controller or @RestController annotation present. + * + * @param controllerClass the controller class to check + * @param checkInheritanceChain if true, the controller inheritance chain will be checked for the target mapping + * annotation + * + * @return true if the class has the @Controller or @RestController annotation present + */ + public static boolean doesClassContainControllerAnnotations(Class controllerClass, boolean checkInheritanceChain) { + if (checkInheritanceChain) { + return AnnotationUtils.findAnnotation(controllerClass, RestController.class) != null || + AnnotationUtils.findAnnotation(controllerClass, Controller.class) != null; + } else { + return controllerClass.getAnnotation(RestController.class) != null || + controllerClass.getAnnotation(Controller.class) != null; + } + } + + /** + * Generate and set a transaction name from a controller's top level and method level mappings. + * + * @param transaction the transaction to set the name for + * @param httpMethod theHTTP method being executed + * @param rootPath the top level controller mapping path + * @param methodPath the method mapping path + */ + public static void assignTransactionNameFromControllerAndMethodRoutes(Transaction transaction, String httpMethod, + String rootPath, String methodPath) { + httpMethod = httpMethod == null ? "GET" : httpMethod; + + String txnName = getRouteName(rootPath, methodPath, httpMethod); + if (NewRelic.getAgent().getLogger().isLoggable(Level.FINEST)) { + NewRelic.getAgent() + .getLogger() + .log(Level.FINEST, "SpringControllerUtility::assignTransactionNameFromControllerAndMethodRoutes (4.3.0): calling transaction.setTransactionName to [{0}] " + + "with FRAMEWORK_HIGH and override false, txn {1}.", txnName, transaction.toString()); + } + + transaction.setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", + txnName); + + } + + /** + * Generate and set a transaction name from a controller class name and method + * + * @param transaction the transaction to set the name for + * @param controllerClass the target controller class + * @param method the method being invoked on the controller + */ + public static void assignTransactionNameFromControllerAndMethod(Transaction transaction, Class controllerClass, Method method) { + String txnName = '/' + getControllerClassAndMethodString(controllerClass, method, false); + + if (NewRelic.getAgent().getLogger().isLoggable(Level.FINEST)) { + NewRelic.getAgent() + .getLogger() + .log(Level.FINEST, "SpringControllerUtility::assignTransactionNameFromControllerAndMethod (4.3.0): " + + "calling transaction.setTransactionName to [{0}] " + + "with FRAMEWORK_HIGH and override false, txn {1}.", txnName, transaction.toString()); + } + + transaction.setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", txnName); + } + + /** + * Return a String composed of the Controller class name + "/" + method name + * + * @param controllerClass the target controller class + * @param method the target method + * @param includePackagePrefix if true, keep the controller class package prefix on the resulting String + * + * @return the String composed of controller class + "/" + method + */ + public static String getControllerClassAndMethodString(Class controllerClass, Method method, boolean includePackagePrefix) { + String result; + if (controllerClass != null && method != null) { + String controllerName = includePackagePrefix ? controllerClass.getName() : controllerClass.getSimpleName(); + int indexOf = controllerName.indexOf(CGLIB_CLASS_SUFFIX); + if (indexOf > 0) { + controllerName = controllerName.substring(0, indexOf); + } + result = controllerName + '/' + method.getName(); + } else { + result = "Unknown"; + } - public static String getPath(String rootPath, String methodPath, RequestMethod httpMethod) { + return result; + } + + /** + * Generate a route name from the given root path, method path and HTTP method. The resulting route name will be: + * /root-mapping/method-mapping (METHOD). For example: api/v1/customer/fetch/{id} (GET) + * + * @param rootPath + * @param methodPath + * @param httpMethod + * @return + */ + private static String getRouteName(String rootPath, String methodPath, String httpMethod) { StringBuilder fullPath = new StringBuilder(); if (rootPath != null && !rootPath.isEmpty()) { if (rootPath.endsWith("/")) { - fullPath.append(rootPath.substring(0, rootPath.length() - 1)); + fullPath.append(rootPath, 0, rootPath.length() - 1); } else { fullPath.append(rootPath); } @@ -33,28 +234,29 @@ public static String getPath(String rootPath, String methodPath, RequestMethod h fullPath.append('/'); } if (methodPath.endsWith("/")) { - fullPath.append(methodPath.substring(0, methodPath.length() - 1)); + fullPath.append(methodPath, 0, methodPath.length() - 1); } else { fullPath.append(methodPath); } } if (httpMethod != null) { - fullPath.append(" (").append(httpMethod.name()).append(')'); + fullPath.append(" (").append(httpMethod).append(')'); } return fullPath.toString(); } /** - * Finds request mapping path. Returns first path of these two: + * Get a path value from one of the mapping annotation's attributes: value or path. If the arrays + * contain more than one String, the first element is used. * - * 1) {@link RequestMapping#value()} - * 2) {@link RequestMapping#path()} + * @param values the values array from the annotation attribute + * @param path the path array from the annotation attribute * - * @return path or null if not found. + * @return a mapping path, from the values or paths arrays */ - public static String getPathValue(String[] values, String[] path) { + private static String getPathValue(String[] values, String[] path) { String result = null; if (values != null) { if (values.length > 0 && !values[0].contains("error.path")) { @@ -69,28 +271,4 @@ public static String getPathValue(String[] values, String[] path) { return result; } - - public static void processAnnotations(Transaction transaction, RequestMethod[] methods, String rootPath, - String methodPath, Class matchedAnnotationClass) { - RequestMethod httpMethod = RequestMethod.GET; - if (methods.length > 0) { - httpMethod = methods[0]; - } - - if (rootPath == null && methodPath == null) { - AgentBridge.getAgent().getLogger().log(Level.FINE, "No path was specified for SpringController {0}", matchedAnnotationClass.getName()); - } else { - String fullPath = SpringControllerUtility.getPath(rootPath, methodPath, httpMethod); - if (NewRelic.getAgent().getLogger().isLoggable(Level.FINEST)) { - NewRelic.getAgent() - .getLogger() - .log(Level.FINEST, "SpringControllerUtility::processAnnotations (4.3.0): calling transaction.setTransactionName to [{0}] " + - "with FRAMEWORK_HIGH and override true, txn {1}, ", - AgentBridge.getAgent().getTransaction().toString()); - } - transaction.setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, true, "SpringController", - fullPath); - } - } - } diff --git a/instrumentation/spring-4.3.0/src/main/java/com/nr/agent/instrumentation/SpringController_Instrumentation.java b/instrumentation/spring-4.3.0/src/main/java/com/nr/agent/instrumentation/SpringController_Instrumentation.java deleted file mode 100644 index 1c30757972..0000000000 --- a/instrumentation/spring-4.3.0/src/main/java/com/nr/agent/instrumentation/SpringController_Instrumentation.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * - * * Copyright 2020 New Relic Corporation. All rights reserved. - * * SPDX-License-Identifier: Apache-2.0 - * - */ - -package com.nr.agent.instrumentation; - -import com.newrelic.agent.bridge.AgentBridge; -import com.newrelic.agent.bridge.Transaction; -import com.newrelic.api.agent.Trace; -import com.newrelic.api.agent.weaver.MatchType; -import com.newrelic.api.agent.weaver.WeaveIntoAllMethods; -import com.newrelic.api.agent.weaver.WeaveWithAnnotation; -import com.newrelic.api.agent.weaver.Weaver; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; - -import java.lang.invoke.MethodHandles; - -import static com.nr.agent.instrumentation.SpringControllerUtility.processAnnotations; - -@WeaveWithAnnotation(annotationClasses = { - "org.springframework.stereotype.Controller", - "org.springframework.web.bind.annotation.RestController" }, - type = MatchType.ExactClass) -public class SpringController_Instrumentation { - - @WeaveWithAnnotation(annotationClasses = { - "org.springframework.web.bind.annotation.RequestMapping", - "org.springframework.web.bind.annotation.PatchMapping", - "org.springframework.web.bind.annotation.PutMapping", - "org.springframework.web.bind.annotation.GetMapping", - "org.springframework.web.bind.annotation.PostMapping", - "org.springframework.web.bind.annotation.DeleteMapping" }) - @WeaveIntoAllMethods - @Trace - private static void requestMapping() { - Transaction transaction = AgentBridge.getAgent().getTransaction(false); - if (transaction != null) { - RequestMapping rootPathMapping = Weaver.getClassAnnotation(RequestMapping.class); - String rootPath = null; - if (rootPathMapping != null) { - rootPath = SpringControllerUtility.getPathValue(rootPathMapping.value(), rootPathMapping.path()); - } - - // the ordering of the following is important. RequestMapping overrides the new annotations. Then it goes: - // PUT, DELETE, POST, GET - if (Weaver.getMethodAnnotation(RequestMapping.class) != null) { - RequestMapping methodPathMapping = Weaver.getMethodAnnotation(RequestMapping.class); - String methodPath = SpringControllerUtility.getPathValue(methodPathMapping.value(), - methodPathMapping.path()); - processAnnotations(transaction, methodPathMapping.method(), rootPath, methodPath, MethodHandles.lookup().lookupClass()); - } else if (Weaver.getMethodAnnotation(PutMapping.class) != null) { - PutMapping methodPathMapping = Weaver.getMethodAnnotation(PutMapping.class); - String methodPath = SpringControllerUtility.getPathValue(methodPathMapping.value(), - methodPathMapping.path()); - processAnnotations(transaction, new RequestMethod[] { RequestMethod.PUT }, rootPath, methodPath, MethodHandles.lookup().lookupClass()); - } else if (Weaver.getMethodAnnotation(DeleteMapping.class) != null) { - DeleteMapping methodPathMapping = Weaver.getMethodAnnotation(DeleteMapping.class); - String methodPath = SpringControllerUtility.getPathValue(methodPathMapping.value(), - methodPathMapping.path()); - processAnnotations(transaction, new RequestMethod[] { RequestMethod.DELETE }, rootPath, methodPath, MethodHandles.lookup().lookupClass()); - } else if (Weaver.getMethodAnnotation(PostMapping.class) != null) { - PostMapping methodPathMapping = Weaver.getMethodAnnotation(PostMapping.class); - String methodPath = SpringControllerUtility.getPathValue(methodPathMapping.value(), - methodPathMapping.path()); - processAnnotations(transaction, new RequestMethod[] { RequestMethod.POST }, rootPath, methodPath, MethodHandles.lookup().lookupClass()); - } else if (Weaver.getMethodAnnotation(PatchMapping.class) != null) { - PatchMapping methodPathMapping = Weaver.getMethodAnnotation(PatchMapping.class); - String methodPath = SpringControllerUtility.getPathValue(methodPathMapping.value(), - methodPathMapping.path()); - processAnnotations(transaction, new RequestMethod[] { RequestMethod.PATCH }, rootPath, methodPath, MethodHandles.lookup().lookupClass()); - } else if (Weaver.getMethodAnnotation(GetMapping.class) != null) { - GetMapping methodPathMapping = Weaver.getMethodAnnotation(GetMapping.class); - String methodPath = SpringControllerUtility.getPathValue(methodPathMapping.value(), - methodPathMapping.path()); - processAnnotations(transaction, new RequestMethod[] { RequestMethod.GET }, rootPath, methodPath, MethodHandles.lookup().lookupClass()); - } - } - } - -} diff --git a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/AbstractHandlerMethodAdapterInstrumentationEnhancedNamingConfigTest_430.java b/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/AbstractHandlerMethodAdapterInstrumentationEnhancedNamingConfigTest_430.java new file mode 100644 index 0000000000..8b5f0d94fa --- /dev/null +++ b/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/AbstractHandlerMethodAdapterInstrumentationEnhancedNamingConfigTest_430.java @@ -0,0 +1,370 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.nr.agent.instrumentation; + +import com.newrelic.agent.bridge.Agent; +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.agent.bridge.TracedMethod; +import com.newrelic.agent.bridge.Transaction; +import com.newrelic.agent.bridge.TransactionNamePriority; +import com.newrelic.api.agent.Config; +import com.newrelic.api.agent.Logger; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.web.method.HandlerMethod; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.logging.Level; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class AbstractHandlerMethodAdapterInstrumentationEnhancedNamingConfigTest_430 { + Agent originalAgent = AgentBridge.getAgent(); + Agent mockAgent = mock(Agent.class); + Logger mockLogger = mock(Logger.class); + Config mockConfig = mock(Config.class); + TracedMethod mockTracedMethod = mock(TracedMethod.class); + + @Before + public void before() throws Exception { + AgentBridge.agent = mockAgent; + when(mockAgent.getConfig()).thenReturn(mockConfig); + when(mockAgent.getLogger()).thenReturn(mockLogger); + when(mockLogger.isLoggable(Level.FINEST)).thenReturn(false); + } + + @After + public void after() { + AgentBridge.agent = originalAgent; + } + + // + // class_transformer.enhanced_spring_transaction_naming set to false + // + + @Test + public void handleInternal_findsAnnotationsFromInterfaceAndMethod_false() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(false); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.ControllerClassWithInterface(), + TestControllerClasses.ControllerClassWithInterface.class.getMethod("get")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/ControllerClassWithInterface/get"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$ControllerClassWithInterface/get"); + } + + @Test + public void handleInternal_findsAnnotationsWithUrlParamFromInterfaceAndMethod_false() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(false); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.ControllerClassWithInterface(), + TestControllerClasses.ControllerClassWithInterface.class.getMethod("getParam")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/ControllerClassWithInterface/getParam"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$ControllerClassWithInterface/getParam"); + + } + + @Test + public void handleInternal_withRequestMappings_findsAnnotationsWithoutInterface_false() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(false); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.StandardControllerWithAllRequestMappings(), + TestControllerClasses.StandardControllerWithAllRequestMappings.class.getMethod("get")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/get (GET)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$StandardControllerWithAllRequestMappings/get"); + } + + @Test + public void handleInternal_withRequestMappingsAndUrlParam_findsAnnotationsWithoutInterface_false() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(false); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.StandardControllerWithAllRequestMappings(), + TestControllerClasses.StandardControllerWithAllRequestMappings.class.getMethod("get2")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/get/{id} (GET)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$StandardControllerWithAllRequestMappings/get2"); + } + + @Test + public void handleInternal_withPostMappings_findsAnnotationsWithoutInterface_false() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(false); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.StandardControllerWithAllRequestMappings(), + TestControllerClasses.StandardControllerWithAllRequestMappings.class.getMethod("post")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("POST"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/post (POST)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$StandardControllerWithAllRequestMappings/post"); + } + + @Test + public void handleInternal_whenNoAnnotationPresent_namesTxnBasedOnControllerClassAndMethod_false() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(false); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.NoAnnotationController(), + TestControllerClasses.NoAnnotationController.class.getMethod("get")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", + "/NoAnnotationController/get"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$NoAnnotationController/get"); + } + + @Test + public void handleInternal_whenExtendingAbstractController_namesTxnBasedOnRouteAndHttpMethod_false() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(false); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.ControllerExtendingAbstractClass(), + TestControllerClasses.ControllerExtendingAbstractClass.class.getMethod("extend")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", + "/ControllerExtendingAbstractClass/extend"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$ControllerExtendingAbstractClass/extend"); + } + + // + // class_transformer.enhanced_spring_transaction_naming set to true + // + + @Test + public void handleInternal_findsAnnotationsFromInterfaceAndMethod_true() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(true); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.ControllerClassWithInterface(), + TestControllerClasses.ControllerClassWithInterface.class.getMethod("get")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/get (GET)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$ControllerClassWithInterface/get"); + } + + @Test + public void handleInternal_findsAnnotationsWithUrlParamFromInterfaceAndMethod_true() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(true); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.ControllerClassWithInterface(), + TestControllerClasses.ControllerClassWithInterface.class.getMethod("getParam")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/get/{id} (GET)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$ControllerClassWithInterface/getParam"); + + } + + @Test + public void handleInternal_withRequestMappings_findsAnnotationsWithoutInterface_true() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(true); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.StandardControllerWithAllRequestMappings(), + TestControllerClasses.StandardControllerWithAllRequestMappings.class.getMethod("get")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/get (GET)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$StandardControllerWithAllRequestMappings/get"); + } + + @Test + public void handleInternal_withRequestMappingsAndUrlParam_findsAnnotationsWithoutInterface_true() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(true); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.StandardControllerWithAllRequestMappings(), + TestControllerClasses.StandardControllerWithAllRequestMappings.class.getMethod("get2")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/get/{id} (GET)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$StandardControllerWithAllRequestMappings/get2"); + } + + @Test + public void handleInternal_withPostMappings_findsAnnotationsWithoutInterface_true() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(true); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.StandardControllerWithAllRequestMappings(), + TestControllerClasses.StandardControllerWithAllRequestMappings.class.getMethod("post")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("POST"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/post (POST)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$StandardControllerWithAllRequestMappings/post"); + } + + @Test + public void handleInternal_whenNoAnnotationPresent_namesTxnBasedOnControllerClassAndMethod_true() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(true); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.NoAnnotationController(), + TestControllerClasses.NoAnnotationController.class.getMethod("get")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", + "/NoAnnotationController/get"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$NoAnnotationController/get"); + } + + @Test + public void handleInternal_whenExtendingAbstractController_namesTxnBasedOnRouteAndHttpMethod_true() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(true); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.ControllerExtendingAbstractClass(), + TestControllerClasses.ControllerExtendingAbstractClass.class.getMethod("extend")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", + "/root/extend (GET)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$ControllerExtendingAbstractClass/extend"); + } +} diff --git a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/App.java b/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/App.java deleted file mode 100644 index 2f5fed5fca..0000000000 --- a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/App.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * - * * Copyright 2020 New Relic Corporation. All rights reserved. - * * SPDX-License-Identifier: Apache-2.0 - * - */ - -package com.nr.agent.instrumentation; - -import java.util.Collections; - -import com.newrelic.api.agent.Trace; - -public class App { - - @Trace(dispatcher = true) - public static void error() { - try { - ErrorPath path = new ErrorPath(); - path.testError(); - } catch (RuntimeException caught) { - System.out.printf("Caught exception"); - } - } - - @Trace(dispatcher = true) - public static String pathClass() { - return new PathClass().testPath(); - } - - @Trace(dispatcher = true) - public static String innerPath() { - return new TestInnerAndDefaultPath().testInnerPath(); - } - - @Trace(dispatcher = true) - public static String methodPath() { - return new TestPathAnnotationForMethod().testPathAnnotation(); - } - - @Trace(dispatcher = true) - public static String nestedValuePath() { - return new NestedValuePath().nestedValuePath(); - } - - @Trace(dispatcher = true) - public static String nestedPathAnnotation() { - return new NestedPathAnnotationTest().nestedPath(); - } - - @Trace(dispatcher = true) - public static String pathAndValue() { - return new PathAndValueTest().pathAndValue(); - } - - @Trace(dispatcher = true) - public static String kotlinDefaultParameter() { - return new KotlinSpringClass().read(Collections.emptyList(), 10); - } - - @Trace(dispatcher = true) - public static String get() { - return new VerbTests().getMapping(); - } - - @Trace(dispatcher = true) - public static String patch() { - return new VerbTests().patchMapping(); - } - - @Trace(dispatcher = true) - public static String post() { - return new VerbTests().postMapping(); - } - - @Trace(dispatcher = true) - public static String put() { - return new VerbTests().putMapping(); - } - - @Trace(dispatcher = true) - public static String delete() { - return new VerbTests().deleteMapping(); - } -} diff --git a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/ErrorPath.java b/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/ErrorPath.java deleted file mode 100644 index e7f78d0a56..0000000000 --- a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/ErrorPath.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * - * * Copyright 2020 New Relic Corporation. All rights reserved. - * * SPDX-License-Identifier: Apache-2.0 - * - */ - -package com.nr.agent.instrumentation; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; - -@Controller -public class ErrorPath { - - @RequestMapping(value = "/errorPath", method = RequestMethod.GET) - public Object testError() { - System.out.printf("throwing exception"); - throw new RuntimeException("test"); - } - - @RequestMapping(value = "/error.pathBad") - @ExceptionHandler(RuntimeException.class) - public void conflict() { - } -} diff --git a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/NestedPathAnnotationTest.java b/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/NestedPathAnnotationTest.java deleted file mode 100644 index 1469350b8e..0000000000 --- a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/NestedPathAnnotationTest.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * - * * Copyright 2020 New Relic Corporation. All rights reserved. - * * SPDX-License-Identifier: Apache-2.0 - * - */ - -package com.nr.agent.instrumentation; - - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; - -@Controller -@RequestMapping(path = "/nestedPath") -public class NestedPathAnnotationTest { - - @RequestMapping(path = "/innerPath") - public String nestedPath() { - return "nestedPathAnnotation"; - } -} diff --git a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/NestedValuePath.java b/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/NestedValuePath.java deleted file mode 100644 index 4bd86a8e17..0000000000 --- a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/NestedValuePath.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * - * * Copyright 2020 New Relic Corporation. All rights reserved. - * * SPDX-License-Identifier: Apache-2.0 - * - */ - -package com.nr.agent.instrumentation; - - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; - -@Controller -@RequestMapping(value = "/valuePath") -public class NestedValuePath { - - @RequestMapping(path = "/innerPath") - public String nestedValuePath() { - return "nestedValuePath"; - } -} \ No newline at end of file diff --git a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/PathAndValueTest.java b/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/PathAndValueTest.java deleted file mode 100644 index 74c29dc913..0000000000 --- a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/PathAndValueTest.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * - * * Copyright 2020 New Relic Corporation. All rights reserved. - * * SPDX-License-Identifier: Apache-2.0 - * - */ - -package com.nr.agent.instrumentation; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; - -@Controller -@RequestMapping(path = "/path") -public class PathAndValueTest { - - @RequestMapping(value = "/value") - public String pathAndValue() { - return "pathAndValue"; - } -} diff --git a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/PathClass.java b/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/PathClass.java deleted file mode 100644 index 4659185706..0000000000 --- a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/PathClass.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * - * * Copyright 2020 New Relic Corporation. All rights reserved. - * * SPDX-License-Identifier: Apache-2.0 - * - */ - -package com.nr.agent.instrumentation; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; - -@Controller -@RequestMapping("/pathClass") -public class PathClass { - - @RequestMapping("/methodTestPath") - public String testPath() { - return "PathClass"; - } -} \ No newline at end of file diff --git a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/SpringControllerTests.java b/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/SpringControllerTests.java deleted file mode 100644 index b9feced0c2..0000000000 --- a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/SpringControllerTests.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * - * * Copyright 2020 New Relic Corporation. All rights reserved. - * * SPDX-License-Identifier: Apache-2.0 - * - */ - -package com.nr.agent.instrumentation; - -import static org.junit.Assert.assertEquals; - -import java.util.Map; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import com.newrelic.agent.introspec.InstrumentationTestConfig; -import com.newrelic.agent.introspec.InstrumentationTestRunner; -import com.newrelic.agent.introspec.Introspector; -import com.newrelic.agent.introspec.TracedMetricData; - -@RunWith(InstrumentationTestRunner.class) -@InstrumentationTestConfig(includePrefixes = { "com.nr.agent.instrumentation" }) -public class SpringControllerTests { - - @Test - public void testErrorPath() { - App.error(); - - Introspector introspector = InstrumentationTestRunner.getIntrospector(); - String expectedTransactionName = "OtherTransaction/SpringController/errorPath (GET)"; - Map metrics = introspector.getMetricsForTransaction(expectedTransactionName); - assertEquals(1, metrics.get("Java/com.nr.agent.instrumentation.ErrorPath/testError").getCallCount()); - } - - @Test - public void testPathClass() { - assertEquals("PathClass", App.pathClass()); - - Introspector introspector = InstrumentationTestRunner.getIntrospector(); - String expectedTransactionName = "OtherTransaction/SpringController/pathClass/methodTestPath (GET)"; - Map metrics = introspector.getMetricsForTransaction(expectedTransactionName); - assertEquals(1, metrics.get("Java/com.nr.agent.instrumentation.PathClass/testPath").getCallCount()); - } - - @Test - public void testInnerPath() { - assertEquals("innerPath", App.innerPath()); - - Introspector introspector = InstrumentationTestRunner.getIntrospector(); - String expectedTransactionName = "OtherTransaction/SpringController/defaultPath/innerPath (GET)"; - Map metrics = introspector.getMetricsForTransaction(expectedTransactionName); - assertEquals(1, metrics.get("Java/com.nr.agent.instrumentation.TestInnerAndDefaultPath/testInnerPath").getCallCount()); - } - - @Test - public void testMethodPath() { - assertEquals("methodPath", App.methodPath()); - - Introspector introspector = InstrumentationTestRunner.getIntrospector(); - String expectedTransactionName = "OtherTransaction/SpringController/pathTest (GET)"; - Map metrics = introspector.getMetricsForTransaction(expectedTransactionName); - assertEquals(1, metrics.get("Java/com.nr.agent.instrumentation.TestPathAnnotationForMethod/testPathAnnotation").getCallCount()); - } - - @Test - public void testNestedValuePath() { - assertEquals("nestedValuePath", App.nestedValuePath()); - - Introspector introspector = InstrumentationTestRunner.getIntrospector(); - String expectedTransactionName = "OtherTransaction/SpringController/valuePath/innerPath (GET)"; - Map metrics = introspector.getMetricsForTransaction(expectedTransactionName); - assertEquals(1, metrics.get("Java/com.nr.agent.instrumentation.NestedValuePath/nestedValuePath").getCallCount()); - } - - @Test - public void testNestedPathAnnotation() { - assertEquals("nestedPathAnnotation", App.nestedPathAnnotation()); - - Introspector introspector = InstrumentationTestRunner.getIntrospector(); - String expectedTransactionName = "OtherTransaction/SpringController/nestedPath/innerPath (GET)"; - Map metrics = introspector.getMetricsForTransaction(expectedTransactionName); - assertEquals(1, metrics.get("Java/com.nr.agent.instrumentation.NestedPathAnnotationTest/nestedPath").getCallCount()); - } - - @Test - public void testPathAndValue() { - assertEquals("pathAndValue", App.pathAndValue()); - - Introspector introspector = InstrumentationTestRunner.getIntrospector(); - String expectedTransactionName = "OtherTransaction/SpringController/path/value (GET)"; - Map metrics = introspector.getMetricsForTransaction(expectedTransactionName); - assertEquals(1, metrics.get("Java/com.nr.agent.instrumentation.PathAndValueTest/pathAndValue").getCallCount()); - } - - @Test - public void testKotlinDefaultParameter() { - assertEquals("kotlinDefaultParameter", App.kotlinDefaultParameter()); - - Introspector introspector = InstrumentationTestRunner.getIntrospector(); - String expectedTransactionName = "OtherTransaction/SpringController/kotlin/read (GET)"; - Map metrics = introspector.getMetricsForTransaction(expectedTransactionName); - assertEquals(1, metrics.get("Java/com.nr.agent.instrumentation.KotlinSpringClass/read").getCallCount()); - } - - @Test - public void testGet() { - assertEquals("getmapping", App.get()); - checkVerb("Get", "getMapping", "GET"); - } - - @Test - public void testPut() { - assertEquals("putmapping", App.put()); - checkVerb("Put", "putMapping", "PUT"); - } - - @Test - public void testPost() { - assertEquals("postmapping", App.post()); - checkVerb("Post", "postMapping", "POST"); - } - - @Test - public void testPatch() { - assertEquals("patchmapping", App.patch()); - checkVerb("Patch", "patchMapping", "PATCH"); - } - - @Test - public void testDelete() { - assertEquals("deletemapping", App.delete()); - checkVerb("Delete", "deleteMapping", "DELETE"); - } - - public void checkVerb(String path, String method, String verb) { - Introspector introspector = InstrumentationTestRunner.getIntrospector(); - String expectedTransactionName = "OtherTransaction/SpringController/verb/" + path + " (" + verb + ")"; - Map metrics = introspector.getMetricsForTransaction(expectedTransactionName); - assertEquals(1, metrics.get("Java/com.nr.agent.instrumentation.VerbTests/" + method).getCallCount()); - } -} diff --git a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/SpringControllerUtilityOtherMethodsTest.java b/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/SpringControllerUtilityOtherMethodsTest.java new file mode 100644 index 0000000000..6705e7bbbd --- /dev/null +++ b/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/SpringControllerUtilityOtherMethodsTest.java @@ -0,0 +1,93 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.nr.agent.instrumentation; + +import com.newrelic.agent.bridge.Transaction; +import com.newrelic.agent.bridge.TransactionNamePriority; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class SpringControllerUtilityOtherMethodsTest { + @Test + public void retrieveRootMappingPathFromController_checkingInheritanceChain_returnsMapping() { + assertEquals("/root", + SpringControllerUtility.retrieveRootMappingPathFromController(TestControllerClasses.StandardController.class, true)); + + assertEquals("/root", + SpringControllerUtility.retrieveRootMappingPathFromController(TestControllerClasses.ControllerClassWithInterface.class, true)); + + assertEquals("/root", + SpringControllerUtility.retrieveRootMappingPathFromController(TestControllerClasses.ControllerExtendingAbstractClass.class, true)); + } + + @Test + public void retrieveRootMappingPathFromController_withoutCheckingInheritanceChain_returnsMappingWhenPresent() { + assertEquals("/root", + SpringControllerUtility.retrieveRootMappingPathFromController(TestControllerClasses.StandardController.class, false)); + + assertNull(SpringControllerUtility.retrieveRootMappingPathFromController(TestControllerClasses.ControllerClassWithInterface.class, false)); + + assertNull(SpringControllerUtility.retrieveRootMappingPathFromController(TestControllerClasses.ControllerExtendingAbstractClass.class, false)); + } + + @Test + public void doesClassContainControllerAnnotations_checkingInheritanceChain_returnsCorrectValue() { + assertTrue(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.StandardController.class, true)); + + assertTrue(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.ControllerClassWithInterface.class, true)); + + assertTrue(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.ControllerExtendingAbstractClass.class, true)); + + assertFalse(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.NoAnnotationController.class, true)); + } + + @Test + public void doesClassContainControllerAnnotations_withoutCheckingInheritanceChain_returnsCorrectValue() { + assertTrue(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.StandardController.class, false)); + + assertFalse(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.ControllerClassWithInterface.class, false)); + + assertFalse(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.ControllerExtendingAbstractClass.class, false)); + + assertFalse(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.NoAnnotationController.class, false)); + } + + @Test + public void assignTransactionNameFromControllerAndMethodRoutes_assignsProperName() { + Transaction mockTxn = mock(Transaction.class); + SpringControllerUtility.assignTransactionNameFromControllerAndMethodRoutes(mockTxn, "GET", "/root", "/get"); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", + "/root/get (GET)"); + } + + @Test + public void assignTransactionNameFromControllerAndMethod_assignsProperName() throws NoSuchMethodException { + Transaction mockTxn = mock(Transaction.class); + SpringControllerUtility.assignTransactionNameFromControllerAndMethod(mockTxn, TestControllerClasses.StandardController.class, + TestControllerClasses.StandardController.class.getMethod("get")); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", + "/StandardController/get"); + } + + @Test + public void getControllerClassAndMethodString_includingPrefix_returnsProperName() throws NoSuchMethodException { + assertEquals("com.nr.agent.instrumentation.TestControllerClasses$StandardController/get", SpringControllerUtility.getControllerClassAndMethodString(TestControllerClasses.StandardController.class, + TestControllerClasses.StandardController.class.getMethod("get"), true)); + } + + @Test + public void getControllerClassAndMethodString_notIncludingPrefix_returnsProperName() throws NoSuchMethodException { + assertEquals("StandardController/get", SpringControllerUtility.getControllerClassAndMethodString(TestControllerClasses.StandardController.class, + TestControllerClasses.StandardController.class.getMethod("get"), false)); + } +} diff --git a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/SpringControllerUtilityRetrieveMappingTest.java b/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/SpringControllerUtilityRetrieveMappingTest.java new file mode 100644 index 0000000000..52cc4179f8 --- /dev/null +++ b/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/SpringControllerUtilityRetrieveMappingTest.java @@ -0,0 +1,75 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.nr.agent.instrumentation; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; + +@RunWith(value = Parameterized.class) +public class SpringControllerUtilityRetrieveMappingTest { + private final String httpMethod; + private final String expectedPath; + + @Parameterized.Parameters(name = "{index}: retrieveMappingPathFromHandlerMethod(httpMethod: {0}) == path value: {1}") + public static Collection data() { + return Arrays.asList(new Object[][]{ + {"GET", "/get"}, + {"PUT", "/put"}, + {"PATCH", "/patch"}, + {"POST", "/post"}, + {"DELETE", "/delete"} + }); + } + + public SpringControllerUtilityRetrieveMappingTest(String httpMethod, String expectedPath) { + this.httpMethod = httpMethod; + this.expectedPath = expectedPath; + } + + @Test + public void retrieveMappingPathFromHandlerMethod_withValidHttpMethodStrings() throws NoSuchMethodException { + Method method = TestControllerClasses.StandardController.class.getMethod(httpMethod.toLowerCase()); + String path = SpringControllerUtility.retrieveMappingPathFromHandlerMethod(method, httpMethod, true); + Assert.assertEquals(expectedPath, path); + } + + @Test + public void retrieveMappingPathFromHandlerMethod_withUnknownHttpMethod_returnsNullPath() throws NoSuchMethodException { + Method method = TestControllerClasses.StandardController.class.getMethod(this.httpMethod.toLowerCase()); + Assert.assertNull(SpringControllerUtility.retrieveMappingPathFromHandlerMethod(method, "Unknown", true)); + } + + @RequestMapping("/root") + public static class MyController { + @GetMapping("/get") + public void get() {} + + @PostMapping("/post") + public void post() {} + + @DeleteMapping("/delete") + public void delete() {} + + @PutMapping("/put") + public void put() {} + + @PatchMapping("/patch") + public void patch() {} + } +} diff --git a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/TestControllerClasses.java b/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/TestControllerClasses.java new file mode 100644 index 0000000000..4d9ddcb167 --- /dev/null +++ b/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/TestControllerClasses.java @@ -0,0 +1,100 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.nr.agent.instrumentation; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +//Interfaces/classes used to test various mapping annotation scenarios +public class TestControllerClasses { + @RequestMapping(value = "/root") + @RestController + @Controller + public static class StandardController { + @GetMapping("/get") + public void get() {} + @PostMapping("/post") + public void post() {} + @DeleteMapping("/delete") + public void delete() {} + @PutMapping("/put") + public void put() {} + @PatchMapping("/patch") + public void patch() {} + } + + @RequestMapping(value = "/root") + @RestController + @Controller + public static class StandardControllerWithAllRequestMappings { + @RequestMapping("/get") + public void get() {} + @RequestMapping("/get/{id}") + public void get2() {} + @RequestMapping("/post") + public void post() {} + @RequestMapping("/delete") + public void delete() {} + @RequestMapping("/put") + public void put() {} + @RequestMapping("/patch") + public void patch() {} + } + + @RequestMapping("/root") + @RestController + @Controller + public interface ControllerInterface { + @GetMapping("/get") + void get(); + @PostMapping("/post") + void post(); + @DeleteMapping("delete") + void delete(); + @RequestMapping("/req") + void req(); + @GetMapping("/get/{id}") + void getParam(); + } + + public static class ControllerClassWithInterface implements ControllerInterface { + @Override + public void get() {} + @Override + public void post() {} + @Override + public void delete() {} + @Override + public void req() {} + @Override + public void getParam() {} + } + + @RequestMapping(value = "/root") + @RestController + @Controller + public static abstract class ControllerToExtend { + @GetMapping("/extend") + abstract public String extend(); + } + + public static class ControllerExtendingAbstractClass extends ControllerToExtend { + public String extend() { + return "extend"; + } + } + + public static class NoAnnotationController { + public void get() {} + } +} diff --git a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/TestInnerAndDefaultPath.java b/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/TestInnerAndDefaultPath.java deleted file mode 100644 index 6bff53a6a8..0000000000 --- a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/TestInnerAndDefaultPath.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * - * * Copyright 2020 New Relic Corporation. All rights reserved. - * * SPDX-License-Identifier: Apache-2.0 - * - */ - -package com.nr.agent.instrumentation; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; - - -@Controller -@RequestMapping("/defaultPath") -public class TestInnerAndDefaultPath { - - @RequestMapping(path = "/innerPath") - public String testInnerPath() { - return "innerPath"; - } -} \ No newline at end of file diff --git a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/TestPathAnnotationForMethod.java b/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/TestPathAnnotationForMethod.java deleted file mode 100644 index ed2042a4f5..0000000000 --- a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/TestPathAnnotationForMethod.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * - * * Copyright 2020 New Relic Corporation. All rights reserved. - * * SPDX-License-Identifier: Apache-2.0 - * - */ - -package com.nr.agent.instrumentation; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; - -@Controller -public class TestPathAnnotationForMethod { - - @RequestMapping(path = "/pathTest") - public String testPathAnnotation() { - return "methodPath"; - } -} diff --git a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/VerbTests.java b/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/VerbTests.java deleted file mode 100644 index 4909c2881f..0000000000 --- a/instrumentation/spring-4.3.0/src/test/java/com/nr/agent/instrumentation/VerbTests.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * - * * Copyright 2020 New Relic Corporation. All rights reserved. - * * SPDX-License-Identifier: Apache-2.0 - * - */ - -package com.nr.agent.instrumentation; - -import com.newrelic.api.agent.Trace; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestMapping; - -@Controller -@RequestMapping(path = "/verb") -public class VerbTests { - @GetMapping(path = "/Get") - public String getMapping() { - return "getmapping"; - } - - @PatchMapping(path = "/Patch") - public String patchMapping() { - return "patchmapping"; - } - - @PostMapping(path = "/Post") - public String postMapping() { - return "postmapping"; - } - - @PutMapping(path = "/Put") - public String putMapping() { - return "putmapping"; - } - - @DeleteMapping(path = "/Delete") - public String deleteMapping() { - return "deletemapping"; - } - - -} diff --git a/instrumentation/spring-4.3.0/src/test/kotlin/com/nr/agent/instrumentation/KotlinSpringClass.kt b/instrumentation/spring-4.3.0/src/test/kotlin/com/nr/agent/instrumentation/KotlinSpringClass.kt deleted file mode 100644 index 13a1efe3de..0000000000 --- a/instrumentation/spring-4.3.0/src/test/kotlin/com/nr/agent/instrumentation/KotlinSpringClass.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * - * * Copyright 2020 New Relic Corporation. All rights reserved. - * * SPDX-License-Identifier: Apache-2.0 - * - */ - -package com.nr.agent.instrumentation - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; - -@Controller -@RequestMapping(path = arrayOf("/kotlin")) -class KotlinSpringClass constructor() { - - @RequestMapping(value = ["/read"]) - fun read(data: List, defaultParam: Int = 0): String { - for (string in data) { - System.out.println(string) - } - return "kotlinDefaultParameter" - } - -} diff --git a/instrumentation/spring-6.0.0/README.md b/instrumentation/spring-6.0.0/README.md new file mode 100644 index 0000000000..678e8d7323 --- /dev/null +++ b/instrumentation/spring-6.0.0/README.md @@ -0,0 +1,98 @@ +# spring-6.0.0 Instrumentation Module + +This module provides instrumentation for Spring Controllers utilizing Spring Web-MVC v6.0.0 to latest. It is identical to the spring-4.3.0 +module aside from the `javax` to `jakarta` namespace change. + +### Traditional Spring Controllers +The module will name transactions based on the controller mapping and HTTP method under the following scenarios: +- Single Spring controller class annotated with/without a class level `@RequestMapping` annotation and methods annotated + with `@RequestMapping`, `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping` or `@PatchMapping`. +```java +@RestController +@RequestMapping("/root") +public class MyController { + @GetMapping("/doGet") + public String handleGet() { + //Do something + } +} +``` + +- A Spring controller class that implements an interface with/without an interface level `@RequestMapping` annotation and methods annotated + with `@RequestMapping`, `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping` or `@PatchMapping`. In addition, the controller class + can also implement methods not on the interface with the same annotations. +```java +@RequestMapping("/root") +public interface MyControllerInterface { + @GetMapping("/doGet/{id}") + String get(@PathVariable String id); + + @PostMapping("/doPost") + String post(); +} + +@RestController +public class MyController implements MyControllerInterface { + @Override + String get(@PathVariable String id) { + //Do something + } + + @Override + String post() { + //Do something + } + + //Method not defined in the interface + @DeleteMapping("/doDelete") + public String delete() { + //Do something + } +} +``` + +- A Spring controller class that extends another controller class with/without a class level `@RequestMapping` annotation and methods annotated + with `@RequestMapping`, `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping` or `@PatchMapping`. In addition, the controller class + can also implement methods not on the parent controller with the same annotations. +```java +@RequestMapping("/root") +public abstract class MyCommonController { + @GetMapping("/doGet") + abstract public String doGet(); +} + +@RestController +public class MyController extends MyCommonController { + @Override + public String doGet() { + //Do something + } +} +``` + +- A Spring controller annotated with a custom annotation which itself is annotated with `@Controller` or `@RestController` and methods annotated + with `@RequestMapping`, `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping` or `@PatchMapping`. +```java +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE}) +@RestController +public @interface CustomRestControllerAnnotation { + //.... +} + +@CustomRestControllerAnnotation +public class TestControllerWithCustomAnnotation { + @GetMapping("/custom") + public String doGet() { + //Do something + } +} + +``` + +The resulting transaction name will be the defined mapping route plus the HTTP method. For example: `root/doGet/{id} (GET)`. + +### Other Controllers Invoked via DispatcherServlet + +For any other controllers invoked via the `DispatcherServlet` ([Actuator](https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.enabling) endpoints, for example) +will be named based on the controller class name and the executed method. For example: `NonStandardController/myMethod`. diff --git a/instrumentation/spring-6.0.0/build.gradle b/instrumentation/spring-6.0.0/build.gradle new file mode 100644 index 0000000000..dcc358ad2d --- /dev/null +++ b/instrumentation/spring-6.0.0/build.gradle @@ -0,0 +1,40 @@ +plugins { + id "org.jetbrains.kotlin.jvm" +} + +dependencies { + implementation(project(":agent-bridge")) + implementation("org.springframework:spring-webmvc:6.0.0") + implementation('jakarta.servlet:jakarta.servlet-api:5.0.0') +} + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.spring-6.0.0', + 'Implementation-Title-Alias': 'spring_annotations', + 'Weave-Violation-Filter': 'METHOD_MISSING_REQUIRED_ANNOTATIONS,CLASS_MISSING_REQUIRED_ANNOTATIONS' } +} + +verifyInstrumentation { + passesOnly('org.springframework:spring-webmvc:[6.0.0,)') { + implementation('jakarta.servlet:jakarta.servlet-api:5.0.0') + } + excludeRegex 'org.springframework:spring-webmvc:.*(RC|SEC|M)[0-9]*$' +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +test { + // These instrumentation tests only run on Java 17+ regardless of the -PtestN gradle property that is set. + onlyIf { + !project.hasProperty('test8') && !project.hasProperty('test11') + } +} + +site { + title 'Spring' + type 'Framework' +} \ No newline at end of file diff --git a/instrumentation/spring-6.0.0/src/main/java/com/nr/agent/instrumentation/AbstractHandlerMethodAdapter_Instrumentation.java b/instrumentation/spring-6.0.0/src/main/java/com/nr/agent/instrumentation/AbstractHandlerMethodAdapter_Instrumentation.java new file mode 100644 index 0000000000..8cb44d38e3 --- /dev/null +++ b/instrumentation/spring-6.0.0/src/main/java/com/nr/agent/instrumentation/AbstractHandlerMethodAdapter_Instrumentation.java @@ -0,0 +1,81 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.nr.agent.instrumentation; + +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.agent.bridge.Transaction; +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.ModelAndView; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Weave(type = MatchType.BaseClass, originalName = "org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter") +public class AbstractHandlerMethodAdapter_Instrumentation { + @Trace + protected ModelAndView handleInternal(HttpServletRequest request, + HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { + Transaction transaction = AgentBridge.getAgent().getTransaction(false); + + if (transaction != null) { + Class controllerClass = handlerMethod.getBeanType(); + Method controllerMethod = handlerMethod.getMethod(); + + //If this setting is false, attempt to name transactions the way the legacy point cut + //named them + boolean isEnhancedNaming = + NewRelic.getAgent().getConfig().getValue("class_transformer.enhanced_spring_transaction_naming", false); + + String httpMethod = request.getMethod(); + if (httpMethod != null) { + httpMethod = httpMethod.toUpperCase(); + } else { + httpMethod = "Unknown"; + } + + //Optimization - If a class doesn't have @Controller/@RestController directly on the controller class + //the transaction is named in point cut style (with enhanced naming set to false) + if (!isEnhancedNaming && !SpringControllerUtility.doesClassContainControllerAnnotations(controllerClass, false)) { + SpringControllerUtility.assignTransactionNameFromControllerAndMethod(transaction, controllerClass, controllerMethod); + } else { //Normal flow to check for annotations based on enhanced naming config flag + String rootPath; + String methodPath; + + //From this point, look for annotations on the class/method, respecting the config flag that controls if the + //annotation has to exist directly on the class/method or can be inherited. + + //Handle typical controller methods with class and method annotations. Those annotations + //can come from implemented interfaces, extended controller classes or be on the controller class itself. + //Note that only RequestMapping mapping annotations can apply to a class (not Get/Post/etc) + rootPath = SpringControllerUtility.retrieveRootMappingPathFromController(controllerClass, isEnhancedNaming); + + //Retrieve the mapping that applies to the target method + methodPath = SpringControllerUtility.retrieveMappingPathFromHandlerMethod(controllerMethod, httpMethod, isEnhancedNaming); + + if (rootPath != null || methodPath != null) { + SpringControllerUtility.assignTransactionNameFromControllerAndMethodRoutes(transaction, httpMethod, rootPath, methodPath); + } else { + //Name based on class + method + SpringControllerUtility.assignTransactionNameFromControllerAndMethod(transaction, controllerClass, controllerMethod); + } + } + transaction.getTracedMethod().setMetricName("Spring", "Java", + SpringControllerUtility.getControllerClassAndMethodString(controllerClass, controllerMethod, true)); + } + + return Weaver.callOriginal(); + } +} diff --git a/instrumentation/spring-6.0.0/src/main/java/com/nr/agent/instrumentation/GetMapping_AntiSkip.java b/instrumentation/spring-6.0.0/src/main/java/com/nr/agent/instrumentation/GetMapping_AntiSkip.java new file mode 100644 index 0000000000..a59f27bc4e --- /dev/null +++ b/instrumentation/spring-6.0.0/src/main/java/com/nr/agent/instrumentation/GetMapping_AntiSkip.java @@ -0,0 +1,16 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation; + +import com.newrelic.api.agent.weaver.SkipIfPresent; +import com.newrelic.api.agent.weaver.Weave; + +// this exists in Spring 4.3 and on but not before +@Weave(originalName = "org.springframework.web.bind.annotation.GetMapping") +public class GetMapping_AntiSkip { +} diff --git a/instrumentation/spring-6.0.0/src/main/java/com/nr/agent/instrumentation/SpringControllerUtility.java b/instrumentation/spring-6.0.0/src/main/java/com/nr/agent/instrumentation/SpringControllerUtility.java new file mode 100644 index 0000000000..95db3fb5de --- /dev/null +++ b/instrumentation/spring-6.0.0/src/main/java/com/nr/agent/instrumentation/SpringControllerUtility.java @@ -0,0 +1,274 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.nr.agent.instrumentation; + +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.agent.bridge.Transaction; +import com.newrelic.agent.bridge.TransactionNamePriority; +import com.newrelic.api.agent.NewRelic; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.lang.reflect.Method; +import java.util.logging.Level; + +public class SpringControllerUtility { + private static final String CGLIB_CLASS_SUFFIX = "$$EnhancerBy"; + + /** + * Return the top level path String on the target controller class, determined by a @RequestMapping annotation. + * This includes any @RequestMapping annotations present on an implemented interface or extended controller class. + * + * @param controllerClass the controller class to search for a @RequestMapping annotation + * @param checkInheritanceChain if true, the controller inheritance chain will be checked for the @RequestMapping + * annotation + * + * @return the path if available; null otherwise + */ + public static String retrieveRootMappingPathFromController(Class controllerClass, boolean checkInheritanceChain) { + RequestMapping rootPathMapping = checkInheritanceChain ? + AnnotationUtils.findAnnotation(controllerClass, RequestMapping.class): + controllerClass.getAnnotation(RequestMapping.class); + + return rootPathMapping != null ? SpringControllerUtility.getPathValue(rootPathMapping.value(), rootPathMapping.path()) : null; + } + + /** + * Return the mapping path for the target method by looking for the XXXXMapping annotation on the method based on the + * supplied httpMethod value. These include annotations present on implemented interface methods or methods + * implemented/overridden from extended classes. + * + * @param method the method to search + * @param httpMethod the HTTP method (verb) being invoked (GET, POST, etc) + * @param checkInheritanceChain if true, the inheritance chain will be checked for the @XXXXMapping + * annotations + * + * @return the path if available; null otherwise + */ + public static String retrieveMappingPathFromHandlerMethod(Method method, String httpMethod, boolean checkInheritanceChain) { + //Check for a generic RequestMapping annotation. If nothing is found, do a targeted search for the annotation + //based on the httpMethod value. + RequestMapping requestMapping = checkInheritanceChain ? + AnnotationUtils.findAnnotation(method, RequestMapping.class) : + method.getAnnotation(RequestMapping.class); + + if (requestMapping != null) { + String pathValue = getPathValue(requestMapping.value(), requestMapping.path()); + if (pathValue != null) { + return pathValue; + } + } + + switch (httpMethod) { + case "PUT": + PutMapping putMapping = checkInheritanceChain ? + AnnotationUtils.findAnnotation(method, PutMapping.class) : + method.getAnnotation(PutMapping.class); + + if (putMapping != null) { + return getPathValue(putMapping.value(), putMapping.path()); + } + break; + case "DELETE": + DeleteMapping deleteMapping = checkInheritanceChain ? + AnnotationUtils.findAnnotation(method, DeleteMapping.class) : + method.getAnnotation(DeleteMapping.class); + if (deleteMapping != null) { + return getPathValue(deleteMapping.value(), deleteMapping.path()); + } + break; + case "POST": + PostMapping postMapping = checkInheritanceChain ? + AnnotationUtils.findAnnotation(method, PostMapping.class) : + method.getAnnotation(PostMapping.class); + + if (postMapping != null) { + return getPathValue(postMapping.value(), postMapping.path()); + } + break; + case "PATCH": + PatchMapping patchMapping = checkInheritanceChain ? + AnnotationUtils.findAnnotation(method, PatchMapping.class) : + method.getAnnotation(PatchMapping.class); + + if (patchMapping != null) { + return getPathValue(patchMapping.value(), patchMapping.path()); + } + break; + case "GET": + GetMapping getMapping = checkInheritanceChain ? + AnnotationUtils.findAnnotation(method, GetMapping.class) : + method.getAnnotation(GetMapping.class); + + if (getMapping != null) { + return getPathValue(getMapping.value(), getMapping.path()); + } + break; + } + + return null; + } + + /** + * Check if the supplied controller class has the @Controller or @RestController annotation present. + * + * @param controllerClass the controller class to check + * @param checkInheritanceChain if true, the controller inheritance chain will be checked for the target mapping + * annotation + * + * @return true if the class has the @Controller or @RestController annotation present + */ + public static boolean doesClassContainControllerAnnotations(Class controllerClass, boolean checkInheritanceChain) { + if (checkInheritanceChain) { + return AnnotationUtils.findAnnotation(controllerClass, RestController.class) != null || + AnnotationUtils.findAnnotation(controllerClass, Controller.class) != null; + } else { + return controllerClass.getAnnotation(RestController.class) != null || + controllerClass.getAnnotation(Controller.class) != null; + } + } + + /** + * Generate and set a transaction name from a controller's top level and method level mappings. + * + * @param transaction the transaction to set the name for + * @param httpMethod theHTTP method being executed + * @param rootPath the top level controller mapping path + * @param methodPath the method mapping path + */ + public static void assignTransactionNameFromControllerAndMethodRoutes(Transaction transaction, String httpMethod, + String rootPath, String methodPath) { + httpMethod = httpMethod == null ? "GET" : httpMethod; + + String txnName = getRouteName(rootPath, methodPath, httpMethod); + if (NewRelic.getAgent().getLogger().isLoggable(Level.FINEST)) { + NewRelic.getAgent() + .getLogger() + .log(Level.FINEST, "SpringControllerUtility::assignTransactionNameFromControllerAndMethodRoutes (6.0.0): calling transaction.setTransactionName to [{0}] " + + "with FRAMEWORK_HIGH and override false, txn {1}.", txnName, transaction.toString()); + } + + transaction.setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", + txnName); + + } + + /** + * Generate and set a transaction name from a controller class name and method + * + * @param transaction the transaction to set the name for + * @param controllerClass the target controller class + * @param method the method being invoked on the controller + */ + public static void assignTransactionNameFromControllerAndMethod(Transaction transaction, Class controllerClass, Method method) { + String txnName = '/' + getControllerClassAndMethodString(controllerClass, method, false); + + if (NewRelic.getAgent().getLogger().isLoggable(Level.FINEST)) { + NewRelic.getAgent() + .getLogger() + .log(Level.FINEST, "SpringControllerUtility::assignTransactionNameFromControllerAndMethod (6.0.0): " + + "calling transaction.setTransactionName to [{0}] " + + "with FRAMEWORK_HIGH and override false, txn {1}.", txnName, transaction.toString()); + } + + transaction.setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", txnName); + } + + /** + * Return a String composed of the Controller class name + "/" + method name + * + * @param controllerClass the target controller class + * @param method the target method + * @param includePackagePrefix if true, keep the controller class package prefix on the resulting String + * + * @return the String composed of controller class + "/" + method + */ + public static String getControllerClassAndMethodString(Class controllerClass, Method method, boolean includePackagePrefix) { + String result; + if (controllerClass != null && method != null) { + String controllerName = includePackagePrefix ? controllerClass.getName() : controllerClass.getSimpleName(); + int indexOf = controllerName.indexOf(CGLIB_CLASS_SUFFIX); + if (indexOf > 0) { + controllerName = controllerName.substring(0, indexOf); + } + result = controllerName + '/' + method.getName(); + } else { + result = "Unknown"; + } + + return result; + } + + /** + * Generate a route name from the given root path, method path and HTTP method. The resulting route name will be: + * /root-mapping/method-mapping (METHOD). For example: api/v1/customer/fetch/{id} (GET) + * + * @param rootPath + * @param methodPath + * @param httpMethod + * @return + */ + private static String getRouteName(String rootPath, String methodPath, String httpMethod) { + StringBuilder fullPath = new StringBuilder(); + if (rootPath != null && !rootPath.isEmpty()) { + if (rootPath.endsWith("/")) { + fullPath.append(rootPath, 0, rootPath.length() - 1); + } else { + fullPath.append(rootPath); + } + } + + if (methodPath != null && !methodPath.isEmpty()) { + if (!methodPath.startsWith("/")) { + fullPath.append('/'); + } + if (methodPath.endsWith("/")) { + fullPath.append(methodPath, 0, methodPath.length() - 1); + } else { + fullPath.append(methodPath); + } + } + + if (httpMethod != null) { + fullPath.append(" (").append(httpMethod).append(')'); + } + + return fullPath.toString(); + } + + /** + * Get a path value from one of the mapping annotation's attributes: value or path. If the arrays + * contain more than one String, the first element is used. + * + * @param values the values array from the annotation attribute + * @param path the path array from the annotation attribute + * + * @return a mapping path, from the values or paths arrays + */ + private static String getPathValue(String[] values, String[] path) { + String result = null; + if (values != null) { + if (values.length > 0 && !values[0].contains("error.path")) { + result = values[0]; + } + if (result == null && path != null) { + if (path.length > 0 && !path[0].contains("error.path")) { + result = path[0]; + } + } + } + + return result; + } +} diff --git a/instrumentation/spring-6.0.0/src/test/java/com/nr/agent/instrumentation/AbstractHandlerMethodAdapterInstrumentationEnhancedNamingConfigTest_600.java b/instrumentation/spring-6.0.0/src/test/java/com/nr/agent/instrumentation/AbstractHandlerMethodAdapterInstrumentationEnhancedNamingConfigTest_600.java new file mode 100644 index 0000000000..96b44bed22 --- /dev/null +++ b/instrumentation/spring-6.0.0/src/test/java/com/nr/agent/instrumentation/AbstractHandlerMethodAdapterInstrumentationEnhancedNamingConfigTest_600.java @@ -0,0 +1,369 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.nr.agent.instrumentation; + +import com.newrelic.agent.bridge.Agent; +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.agent.bridge.TracedMethod; +import com.newrelic.agent.bridge.Transaction; +import com.newrelic.agent.bridge.TransactionNamePriority; +import com.newrelic.api.agent.Config; +import com.newrelic.api.agent.Logger; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.web.method.HandlerMethod; + +import java.util.logging.Level; + +import static org.mockito.Mockito.*; + +public class AbstractHandlerMethodAdapterInstrumentationEnhancedNamingConfigTest_600 { + Agent originalAgent = AgentBridge.getAgent(); + Agent mockAgent = mock(Agent.class); + Logger mockLogger = mock(Logger.class); + Config mockConfig = mock(Config.class); + TracedMethod mockTracedMethod = mock(TracedMethod.class); + + @Before + public void before() throws Exception { + AgentBridge.agent = mockAgent; + when(mockAgent.getConfig()).thenReturn(mockConfig); + when(mockAgent.getLogger()).thenReturn(mockLogger); + when(mockLogger.isLoggable(Level.FINEST)).thenReturn(false); + } + + @After + public void after() { + AgentBridge.agent = originalAgent; + } + + // + // class_transformer.enhanced_spring_transaction_naming set to false + // + + @Test + public void handleInternal_findsAnnotationsFromInterfaceAndMethod_false() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(false); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.ControllerClassWithInterface(), + TestControllerClasses.ControllerClassWithInterface.class.getMethod("get")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/ControllerClassWithInterface/get"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$ControllerClassWithInterface/get"); + } + + @Test + public void handleInternal_findsAnnotationsWithUrlParamFromInterfaceAndMethod_false() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(false); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.ControllerClassWithInterface(), + TestControllerClasses.ControllerClassWithInterface.class.getMethod("getParam")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/ControllerClassWithInterface/getParam"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$ControllerClassWithInterface/getParam"); + + } + + @Test + public void handleInternal_withRequestMappings_findsAnnotationsWithoutInterface_false() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(false); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.StandardControllerWithAllRequestMappings(), + TestControllerClasses.StandardControllerWithAllRequestMappings.class.getMethod("get")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/get (GET)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$StandardControllerWithAllRequestMappings/get"); + } + + @Test + public void handleInternal_withRequestMappingsAndUrlParam_findsAnnotationsWithoutInterface_false() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(false); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.StandardControllerWithAllRequestMappings(), + TestControllerClasses.StandardControllerWithAllRequestMappings.class.getMethod("get2")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/get/{id} (GET)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$StandardControllerWithAllRequestMappings/get2"); + } + + @Test + public void handleInternal_withPostMappings_findsAnnotationsWithoutInterface_false() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(false); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.StandardControllerWithAllRequestMappings(), + TestControllerClasses.StandardControllerWithAllRequestMappings.class.getMethod("post")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("POST"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/post (POST)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$StandardControllerWithAllRequestMappings/post"); + } + + @Test + public void handleInternal_whenNoAnnotationPresent_namesTxnBasedOnControllerClassAndMethod_false() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(false); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.NoAnnotationController(), + TestControllerClasses.NoAnnotationController.class.getMethod("get")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", + "/NoAnnotationController/get"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$NoAnnotationController/get"); + } + + @Test + public void handleInternal_whenExtendingAbstractController_namesTxnBasedOnRouteAndHttpMethod_false() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(false); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.ControllerExtendingAbstractClass(), + TestControllerClasses.ControllerExtendingAbstractClass.class.getMethod("extend")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", + "/ControllerExtendingAbstractClass/extend"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$ControllerExtendingAbstractClass/extend"); + } + + // + // class_transformer.enhanced_spring_transaction_naming set to true + // + + @Test + public void handleInternal_findsAnnotationsFromInterfaceAndMethod_true() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(true); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.ControllerClassWithInterface(), + TestControllerClasses.ControllerClassWithInterface.class.getMethod("get")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/get (GET)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$ControllerClassWithInterface/get"); + } + + @Test + public void handleInternal_findsAnnotationsWithUrlParamFromInterfaceAndMethod_true() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(true); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.ControllerClassWithInterface(), + TestControllerClasses.ControllerClassWithInterface.class.getMethod("getParam")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/get/{id} (GET)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$ControllerClassWithInterface/getParam"); + + } + + @Test + public void handleInternal_withRequestMappings_findsAnnotationsWithoutInterface_true() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(true); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.StandardControllerWithAllRequestMappings(), + TestControllerClasses.StandardControllerWithAllRequestMappings.class.getMethod("get")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/get (GET)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$StandardControllerWithAllRequestMappings/get"); + } + + @Test + public void handleInternal_withRequestMappingsAndUrlParam_findsAnnotationsWithoutInterface_true() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(true); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.StandardControllerWithAllRequestMappings(), + TestControllerClasses.StandardControllerWithAllRequestMappings.class.getMethod("get2")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/get/{id} (GET)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$StandardControllerWithAllRequestMappings/get2"); + } + + @Test + public void handleInternal_withPostMappings_findsAnnotationsWithoutInterface_true() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(true); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.StandardControllerWithAllRequestMappings(), + TestControllerClasses.StandardControllerWithAllRequestMappings.class.getMethod("post")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("POST"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/post (POST)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$StandardControllerWithAllRequestMappings/post"); + } + + @Test + public void handleInternal_whenNoAnnotationPresent_namesTxnBasedOnControllerClassAndMethod_true() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(true); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.NoAnnotationController(), + TestControllerClasses.NoAnnotationController.class.getMethod("get")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", + "/NoAnnotationController/get"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$NoAnnotationController/get"); + } + + @Test + public void handleInternal_whenExtendingAbstractController_namesTxnBasedOnRouteAndHttpMethod_true() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(true); + AbstractHandlerMethodAdapter_Instrumentation cut = new AbstractHandlerMethodAdapter_Instrumentation(); + HandlerMethod handlerMethod = new HandlerMethod(new TestControllerClasses.ControllerExtendingAbstractClass(), + TestControllerClasses.ControllerExtendingAbstractClass.class.getMethod("extend")); + + HttpServletRequest mockReq = mock(HttpServletRequest.class); + HttpServletResponse mockResp = mock(HttpServletResponse.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockReq.getMethod()).thenReturn("GET"); + + cut.handleInternal(mockReq, mockResp, handlerMethod); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", + "/root/extend (GET)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "com.nr.agent.instrumentation.TestControllerClasses$ControllerExtendingAbstractClass/extend"); + } +} diff --git a/instrumentation/spring-6.0.0/src/test/java/com/nr/agent/instrumentation/SpringControllerUtilityOtherMethodsTest.java b/instrumentation/spring-6.0.0/src/test/java/com/nr/agent/instrumentation/SpringControllerUtilityOtherMethodsTest.java new file mode 100644 index 0000000000..a01033542f --- /dev/null +++ b/instrumentation/spring-6.0.0/src/test/java/com/nr/agent/instrumentation/SpringControllerUtilityOtherMethodsTest.java @@ -0,0 +1,89 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.nr.agent.instrumentation; + +import com.newrelic.agent.bridge.Transaction; +import com.newrelic.agent.bridge.TransactionNamePriority; +import org.junit.Test; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +public class SpringControllerUtilityOtherMethodsTest { + @Test + public void retrieveRootMappingPathFromController_checkingInheritanceChain_returnsMapping() { + assertEquals("/root", + SpringControllerUtility.retrieveRootMappingPathFromController(TestControllerClasses.StandardController.class, true)); + + assertEquals("/root", + SpringControllerUtility.retrieveRootMappingPathFromController(TestControllerClasses.ControllerClassWithInterface.class, true)); + + assertEquals("/root", + SpringControllerUtility.retrieveRootMappingPathFromController(TestControllerClasses.ControllerExtendingAbstractClass.class, true)); + } + + @Test + public void retrieveRootMappingPathFromController_withoutCheckingInheritanceChain_returnsMappingWhenPresent() { + assertEquals("/root", + SpringControllerUtility.retrieveRootMappingPathFromController(TestControllerClasses.StandardController.class, false)); + + assertNull(SpringControllerUtility.retrieveRootMappingPathFromController(TestControllerClasses.ControllerClassWithInterface.class, false)); + + assertNull(SpringControllerUtility.retrieveRootMappingPathFromController(TestControllerClasses.ControllerExtendingAbstractClass.class, false)); + } + + @Test + public void doesClassContainControllerAnnotations_checkingInheritanceChain_returnsCorrectValue() { + assertTrue(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.StandardController.class, true)); + + assertTrue(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.ControllerClassWithInterface.class, true)); + + assertTrue(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.ControllerExtendingAbstractClass.class, true)); + + assertFalse(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.NoAnnotationController.class, true)); + } + + @Test + public void doesClassContainControllerAnnotations_withoutCheckingInheritanceChain_returnsCorrectValue() { + assertTrue(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.StandardController.class, false)); + + assertFalse(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.ControllerClassWithInterface.class, false)); + + assertFalse(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.ControllerExtendingAbstractClass.class, false)); + + assertFalse(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.NoAnnotationController.class, false)); + } + + @Test + public void assignTransactionNameFromControllerAndMethodRoutes_assignsProperName() { + Transaction mockTxn = mock(Transaction.class); + SpringControllerUtility.assignTransactionNameFromControllerAndMethodRoutes(mockTxn, "GET", "/root", "/get"); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", + "/root/get (GET)"); + } + + @Test + public void assignTransactionNameFromControllerAndMethod_assignsProperName() throws NoSuchMethodException { + Transaction mockTxn = mock(Transaction.class); + SpringControllerUtility.assignTransactionNameFromControllerAndMethod(mockTxn, TestControllerClasses.StandardController.class, + TestControllerClasses.StandardController.class.getMethod("get")); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", + "/StandardController/get"); + } + + @Test + public void getControllerClassAndMethodString_includingPrefix_returnsProperName() throws NoSuchMethodException { + assertEquals("com.nr.agent.instrumentation.TestControllerClasses$StandardController/get", SpringControllerUtility.getControllerClassAndMethodString(TestControllerClasses.StandardController.class, + TestControllerClasses.StandardController.class.getMethod("get"), true)); + } + + @Test + public void getControllerClassAndMethodString_notIncludingPrefix_returnsProperName() throws NoSuchMethodException { + assertEquals("StandardController/get", SpringControllerUtility.getControllerClassAndMethodString(TestControllerClasses.StandardController.class, + TestControllerClasses.StandardController.class.getMethod("get"), false)); + } +} diff --git a/instrumentation/spring-6.0.0/src/test/java/com/nr/agent/instrumentation/SpringControllerUtilityRetrieveMappingTest.java b/instrumentation/spring-6.0.0/src/test/java/com/nr/agent/instrumentation/SpringControllerUtilityRetrieveMappingTest.java new file mode 100644 index 0000000000..52cc4179f8 --- /dev/null +++ b/instrumentation/spring-6.0.0/src/test/java/com/nr/agent/instrumentation/SpringControllerUtilityRetrieveMappingTest.java @@ -0,0 +1,75 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.nr.agent.instrumentation; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; + +@RunWith(value = Parameterized.class) +public class SpringControllerUtilityRetrieveMappingTest { + private final String httpMethod; + private final String expectedPath; + + @Parameterized.Parameters(name = "{index}: retrieveMappingPathFromHandlerMethod(httpMethod: {0}) == path value: {1}") + public static Collection data() { + return Arrays.asList(new Object[][]{ + {"GET", "/get"}, + {"PUT", "/put"}, + {"PATCH", "/patch"}, + {"POST", "/post"}, + {"DELETE", "/delete"} + }); + } + + public SpringControllerUtilityRetrieveMappingTest(String httpMethod, String expectedPath) { + this.httpMethod = httpMethod; + this.expectedPath = expectedPath; + } + + @Test + public void retrieveMappingPathFromHandlerMethod_withValidHttpMethodStrings() throws NoSuchMethodException { + Method method = TestControllerClasses.StandardController.class.getMethod(httpMethod.toLowerCase()); + String path = SpringControllerUtility.retrieveMappingPathFromHandlerMethod(method, httpMethod, true); + Assert.assertEquals(expectedPath, path); + } + + @Test + public void retrieveMappingPathFromHandlerMethod_withUnknownHttpMethod_returnsNullPath() throws NoSuchMethodException { + Method method = TestControllerClasses.StandardController.class.getMethod(this.httpMethod.toLowerCase()); + Assert.assertNull(SpringControllerUtility.retrieveMappingPathFromHandlerMethod(method, "Unknown", true)); + } + + @RequestMapping("/root") + public static class MyController { + @GetMapping("/get") + public void get() {} + + @PostMapping("/post") + public void post() {} + + @DeleteMapping("/delete") + public void delete() {} + + @PutMapping("/put") + public void put() {} + + @PatchMapping("/patch") + public void patch() {} + } +} diff --git a/instrumentation/spring-6.0.0/src/test/java/com/nr/agent/instrumentation/TestControllerClasses.java b/instrumentation/spring-6.0.0/src/test/java/com/nr/agent/instrumentation/TestControllerClasses.java new file mode 100644 index 0000000000..4d9ddcb167 --- /dev/null +++ b/instrumentation/spring-6.0.0/src/test/java/com/nr/agent/instrumentation/TestControllerClasses.java @@ -0,0 +1,100 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.nr.agent.instrumentation; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +//Interfaces/classes used to test various mapping annotation scenarios +public class TestControllerClasses { + @RequestMapping(value = "/root") + @RestController + @Controller + public static class StandardController { + @GetMapping("/get") + public void get() {} + @PostMapping("/post") + public void post() {} + @DeleteMapping("/delete") + public void delete() {} + @PutMapping("/put") + public void put() {} + @PatchMapping("/patch") + public void patch() {} + } + + @RequestMapping(value = "/root") + @RestController + @Controller + public static class StandardControllerWithAllRequestMappings { + @RequestMapping("/get") + public void get() {} + @RequestMapping("/get/{id}") + public void get2() {} + @RequestMapping("/post") + public void post() {} + @RequestMapping("/delete") + public void delete() {} + @RequestMapping("/put") + public void put() {} + @RequestMapping("/patch") + public void patch() {} + } + + @RequestMapping("/root") + @RestController + @Controller + public interface ControllerInterface { + @GetMapping("/get") + void get(); + @PostMapping("/post") + void post(); + @DeleteMapping("delete") + void delete(); + @RequestMapping("/req") + void req(); + @GetMapping("/get/{id}") + void getParam(); + } + + public static class ControllerClassWithInterface implements ControllerInterface { + @Override + public void get() {} + @Override + public void post() {} + @Override + public void delete() {} + @Override + public void req() {} + @Override + public void getParam() {} + } + + @RequestMapping(value = "/root") + @RestController + @Controller + public static abstract class ControllerToExtend { + @GetMapping("/extend") + abstract public String extend(); + } + + public static class ControllerExtendingAbstractClass extends ControllerToExtend { + public String extend() { + return "extend"; + } + } + + public static class NoAnnotationController { + public void get() {} + } +} diff --git a/instrumentation/spring-webflux-controller-mappings-5.0.0/README.md b/instrumentation/spring-webflux-controller-mappings-5.0.0/README.md new file mode 100644 index 0000000000..c83a226849 --- /dev/null +++ b/instrumentation/spring-webflux-controller-mappings-5.0.0/README.md @@ -0,0 +1,4 @@ +# Spring Webflux Controller Mapping Instrumentation + +This module provides the same transaction naming functionality as the spring-4.3.0 and spring-6.0.0 modules, except for +Webflux endpoints. \ No newline at end of file diff --git a/instrumentation/spring-webflux-controller-mappings-5.0.0/build.gradle b/instrumentation/spring-webflux-controller-mappings-5.0.0/build.gradle new file mode 100644 index 0000000000..fa3551233b --- /dev/null +++ b/instrumentation/spring-webflux-controller-mappings-5.0.0/build.gradle @@ -0,0 +1,30 @@ +dependencies { + implementation(project(":agent-bridge")) + implementation("org.springframework:spring-webflux:5.0.0.RELEASE") + implementation("org.springframework:spring-web:5.0.0.RELEASE") + implementation("org.springframework:spring-context:5.0.0.RELEASE") + implementation project(path: ':newrelic-weaver-api') + + testImplementation("org.springframework:spring-context:5.0.0.RELEASE") + testImplementation("io.projectreactor.ipc:reactor-netty:0.7.8.RELEASE") + + testImplementation(project(":instrumentation:spring-webclient-5.0")) +} + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.spring-webflux-controller-mappings-5.0.0' } +} + +verifyInstrumentation { + passesOnly('org.springframework:spring-webflux:[5.0.0.RELEASE,)') { + implementation('org.springframework:spring-web:5.0.0.RELEASE') + implementation("org.springframework:spring-context:5.0.0.RELEASE") + } + excludeRegex '.*.M[0-9]' + excludeRegex '.*.RC[0-9]' +} + +site { + title 'Spring WebFlux' + type 'Framework' +} diff --git a/instrumentation/spring-webflux-controller-mappings-5.0.0/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod_Instrumentation.java b/instrumentation/spring-webflux-controller-mappings-5.0.0/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod_Instrumentation.java new file mode 100644 index 0000000000..e19dd89367 --- /dev/null +++ b/instrumentation/spring-webflux-controller-mappings-5.0.0/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod_Instrumentation.java @@ -0,0 +1,80 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package org.springframework.web.reactive.result.method; + +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.agent.bridge.Transaction; +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import org.springframework.web.reactive.BindingContext; +import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.lang.reflect.Method; + +@Weave(type = MatchType.ExactClass, originalName = "org.springframework.web.reactive.result.method.InvocableHandlerMethod") +public abstract class InvocableHandlerMethod_Instrumentation { + + abstract protected Method getBridgedMethod(); + + abstract public Class getBeanType(); + + @Trace + public Mono invoke( + ServerWebExchange exchange, BindingContext bindingContext, Object... providedArgs) { + Transaction transaction = AgentBridge.getAgent().getTransaction(false); + + if (transaction != null) { + Class controllerClass = getBeanType(); + Method controllerMethod = getBridgedMethod(); + + //If this setting is false, attempt to name transactions the way the legacy point cut + //named them + boolean isEnhancedNaming = + NewRelic.getAgent().getConfig().getValue("class_transformer.enhanced_spring_transaction_naming", false); + + String httpMethod = exchange.getRequest().getMethod().name(); + if (httpMethod != null) { + httpMethod = httpMethod.toUpperCase(); + } else { + httpMethod = "Unknown"; + } + + //Optimization - If a class doesn't have @Controller/@RestController directly on the controller class + //the transaction is named in point cut style (with enhanced naming set to false) + if (!isEnhancedNaming && !SpringControllerUtility.doesClassContainControllerAnnotations(controllerClass, false)) { + SpringControllerUtility.assignTransactionNameFromControllerAndMethod(transaction, controllerClass, controllerMethod); + } else { + String rootPath; + String methodPath; + + //Handle typical controller methods with class and method annotations. Those annotations + //can come from implemented interfaces, extended controller classes or be on the controller class itself. + //Note that only RequestMapping mapping annotations can apply to a class (not Get/Post/etc) + rootPath = SpringControllerUtility.retrieveRootMappingPathFromController(controllerClass, isEnhancedNaming); + + //Retrieve the mapping that applies to the target method + methodPath = SpringControllerUtility.retrieveMappingPathFromHandlerMethod(controllerMethod, httpMethod, isEnhancedNaming); + + if (rootPath != null || methodPath != null) { + SpringControllerUtility.assignTransactionNameFromControllerAndMethodRoutes(transaction, httpMethod, rootPath, methodPath); + } else { + //Name based on class + method + SpringControllerUtility.assignTransactionNameFromControllerAndMethod(transaction, controllerClass, controllerMethod); + } + } + transaction.getTracedMethod().setMetricName("Spring", "Java", + SpringControllerUtility.getControllerClassAndMethodString(controllerClass, controllerMethod, true)); + } + + return Weaver.callOriginal(); + } +} diff --git a/instrumentation/spring-webflux-controller-mappings-5.0.0/src/main/java/org/springframework/web/reactive/result/method/SpringControllerUtility.java b/instrumentation/spring-webflux-controller-mappings-5.0.0/src/main/java/org/springframework/web/reactive/result/method/SpringControllerUtility.java new file mode 100644 index 0000000000..09be89e00f --- /dev/null +++ b/instrumentation/spring-webflux-controller-mappings-5.0.0/src/main/java/org/springframework/web/reactive/result/method/SpringControllerUtility.java @@ -0,0 +1,273 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package org.springframework.web.reactive.result.method; + +import com.newrelic.agent.bridge.Transaction; +import com.newrelic.agent.bridge.TransactionNamePriority; +import com.newrelic.api.agent.NewRelic; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.lang.reflect.Method; +import java.util.logging.Level; + +public class SpringControllerUtility { + private static final String CGLIB_CLASS_SUFFIX = "$$EnhancerBy"; + + /** + * Return the top level path String on the target controller class, determined by a @RequestMapping annotation. + * This includes any @RequestMapping annotations present on an implemented interface or extended controller class. + * + * @param controllerClass the controller class to search for a @RequestMapping annotation + * @param checkInheritanceChain if true, the controller inheritance chain will be checked for the @RequestMapping + * annotation + * + * @return the path if available; null otherwise + */ + public static String retrieveRootMappingPathFromController(Class controllerClass, boolean checkInheritanceChain) { + RequestMapping rootPathMapping = checkInheritanceChain ? + AnnotationUtils.findAnnotation(controllerClass, RequestMapping.class): + controllerClass.getAnnotation(RequestMapping.class); + + return rootPathMapping != null ? SpringControllerUtility.getPathValue(rootPathMapping.value(), rootPathMapping.path()) : null; + } + + /** + * Return the mapping path for the target method by looking for the XXXXMapping annotation on the method based on the + * supplied httpMethod value. These include annotations present on implemented interface methods or methods + * implemented/overridden from extended classes. + * + * @param method the method to search + * @param httpMethod the HTTP method (verb) being invoked (GET, POST, etc) + * @param checkInheritanceChain if true, the inheritance chain will be checked for the @XXXXMapping + * annotations + * + * @return the path if available; null otherwise + */ + public static String retrieveMappingPathFromHandlerMethod(Method method, String httpMethod, boolean checkInheritanceChain) { + //Check for a generic RequestMapping annotation. If nothing is found, do a targeted search for the annotation + //based on the httpMethod value. + RequestMapping requestMapping = checkInheritanceChain ? + AnnotationUtils.findAnnotation(method, RequestMapping.class) : + method.getAnnotation(RequestMapping.class); + + if (requestMapping != null) { + String pathValue = getPathValue(requestMapping.value(), requestMapping.path()); + if (pathValue != null) { + return pathValue; + } + } + + switch (httpMethod) { + case "PUT": + PutMapping putMapping = checkInheritanceChain ? + AnnotationUtils.findAnnotation(method, PutMapping.class) : + method.getAnnotation(PutMapping.class); + + if (putMapping != null) { + return getPathValue(putMapping.value(), putMapping.path()); + } + break; + case "DELETE": + DeleteMapping deleteMapping = checkInheritanceChain ? + AnnotationUtils.findAnnotation(method, DeleteMapping.class) : + method.getAnnotation(DeleteMapping.class); + if (deleteMapping != null) { + return getPathValue(deleteMapping.value(), deleteMapping.path()); + } + break; + case "POST": + PostMapping postMapping = checkInheritanceChain ? + AnnotationUtils.findAnnotation(method, PostMapping.class) : + method.getAnnotation(PostMapping.class); + + if (postMapping != null) { + return getPathValue(postMapping.value(), postMapping.path()); + } + break; + case "PATCH": + PatchMapping patchMapping = checkInheritanceChain ? + AnnotationUtils.findAnnotation(method, PatchMapping.class) : + method.getAnnotation(PatchMapping.class); + + if (patchMapping != null) { + return getPathValue(patchMapping.value(), patchMapping.path()); + } + break; + case "GET": + GetMapping getMapping = checkInheritanceChain ? + AnnotationUtils.findAnnotation(method, GetMapping.class) : + method.getAnnotation(GetMapping.class); + + if (getMapping != null) { + return getPathValue(getMapping.value(), getMapping.path()); + } + break; + } + + return null; + } + + /** + * Check if the supplied controller class has the @Controller or @RestController annotation present. + * + * @param controllerClass the controller class to check + * @param checkInheritanceChain if true, the controller inheritance chain will be checked for the target mapping + * annotation + * + * @return true if the class has the @Controller or @RestController annotation present + */ + public static boolean doesClassContainControllerAnnotations(Class controllerClass, boolean checkInheritanceChain) { + if (checkInheritanceChain) { + return AnnotationUtils.findAnnotation(controllerClass, RestController.class) != null || + AnnotationUtils.findAnnotation(controllerClass, Controller.class) != null; + } else { + return controllerClass.getAnnotation(RestController.class) != null || + controllerClass.getAnnotation(Controller.class) != null; + } + } + + /** + * Generate and set a transaction name from a controller's top level and method level mappings. + * + * @param transaction the transaction to set the name for + * @param httpMethod theHTTP method being executed + * @param rootPath the top level controller mapping path + * @param methodPath the method mapping path + */ + public static void assignTransactionNameFromControllerAndMethodRoutes(Transaction transaction, String httpMethod, + String rootPath, String methodPath) { + httpMethod = httpMethod == null ? "GET" : httpMethod; + + String txnName = getRouteName(rootPath, methodPath, httpMethod); + if (NewRelic.getAgent().getLogger().isLoggable(Level.FINEST)) { + NewRelic.getAgent() + .getLogger() + .log(Level.FINEST, "SpringControllerUtility::assignTransactionNameFromControllerAndMethodRoutes (webflux-5.0.0): calling transaction.setTransactionName to [{0}] " + + "with FRAMEWORK_HIGH and override false, txn {1}.", txnName, transaction.toString()); + } + + transaction.setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", + txnName); + + } + + /** + * Generate and set a transaction name from a controller class name and method + * + * @param transaction the transaction to set the name for + * @param controllerClass the target controller class + * @param method the method being invoked on the controller + */ + public static void assignTransactionNameFromControllerAndMethod(Transaction transaction, Class controllerClass, Method method) { + String txnName = '/' + getControllerClassAndMethodString(controllerClass, method, false); + + if (NewRelic.getAgent().getLogger().isLoggable(Level.FINEST)) { + NewRelic.getAgent() + .getLogger() + .log(Level.FINEST, "SpringControllerUtility::assignTransactionNameFromControllerAndMethod (webflux-5.0.0): " + + "calling transaction.setTransactionName to [{0}] " + + "with FRAMEWORK_HIGH and override false, txn {1}.", txnName, transaction.toString()); + } + + transaction.setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", txnName); + } + + /** + * Return a String composed of the Controller class name + "/" + method name + * + * @param controllerClass the target controller class + * @param method the target method + * @param includePackagePrefix if true, keep the controller class package prefix on the resulting String + * + * @return the String composed of controller class + "/" + method + */ + public static String getControllerClassAndMethodString(Class controllerClass, Method method, boolean includePackagePrefix) { + String result; + if (controllerClass != null && method != null) { + String controllerName = includePackagePrefix ? controllerClass.getName() : controllerClass.getSimpleName(); + int indexOf = controllerName.indexOf(CGLIB_CLASS_SUFFIX); + if (indexOf > 0) { + controllerName = controllerName.substring(0, indexOf); + } + result = controllerName + '/' + method.getName(); + } else { + result = "Unknown"; + } + + return result; + } + + /** + * Generate a route name from the given root path, method path and HTTP method. The resulting route name will be: + * /root-mapping/method-mapping (METHOD). For example: api/v1/customer/fetch/{id} (GET) + * + * @param rootPath + * @param methodPath + * @param httpMethod + * @return + */ + private static String getRouteName(String rootPath, String methodPath, String httpMethod) { + StringBuilder fullPath = new StringBuilder(); + if (rootPath != null && !rootPath.isEmpty()) { + if (rootPath.endsWith("/")) { + fullPath.append(rootPath, 0, rootPath.length() - 1); + } else { + fullPath.append(rootPath); + } + } + + if (methodPath != null && !methodPath.isEmpty()) { + if (!methodPath.startsWith("/")) { + fullPath.append('/'); + } + if (methodPath.endsWith("/")) { + fullPath.append(methodPath, 0, methodPath.length() - 1); + } else { + fullPath.append(methodPath); + } + } + + if (httpMethod != null) { + fullPath.append(" (").append(httpMethod).append(')'); + } + + return fullPath.toString(); + } + + /** + * Get a path value from one of the mapping annotation's attributes: value or path. If the arrays + * contain more than one String, the first element is used. + * + * @param values the values array from the annotation attribute + * @param path the path array from the annotation attribute + * + * @return a mapping path, from the values or paths arrays + */ + private static String getPathValue(String[] values, String[] path) { + String result = null; + if (values != null) { + if (values.length > 0 && !values[0].contains("error.path")) { + result = values[0]; + } + if (result == null && path != null) { + if (path.length > 0 && !path[0].contains("error.path")) { + result = path[0]; + } + } + } + + return result; + } +} diff --git a/instrumentation/spring-webflux-controller-mappings-5.0.0/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodInstrumentationEnhancedNamingConfigTest_webflux500.java b/instrumentation/spring-webflux-controller-mappings-5.0.0/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodInstrumentationEnhancedNamingConfigTest_webflux500.java new file mode 100644 index 0000000000..8f79278a08 --- /dev/null +++ b/instrumentation/spring-webflux-controller-mappings-5.0.0/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodInstrumentationEnhancedNamingConfigTest_webflux500.java @@ -0,0 +1,380 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package org.springframework.web.reactive.result.method; + +import com.newrelic.agent.bridge.Agent; +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.agent.bridge.TracedMethod; +import com.newrelic.agent.bridge.Transaction; +import com.newrelic.agent.bridge.TransactionNamePriority; +import com.newrelic.api.agent.Config; +import com.newrelic.api.agent.Logger; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.reactive.BindingContext; +import org.springframework.web.server.ServerWebExchange; + +import java.util.logging.Level; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class InvocableHandlerMethodInstrumentationEnhancedNamingConfigTest_webflux500 { + Agent originalAgent = AgentBridge.getAgent(); + Agent mockAgent = mock(Agent.class); + Logger mockLogger = mock(Logger.class); + Config mockConfig = mock(Config.class); + TracedMethod mockTracedMethod = mock(TracedMethod.class); + ServerWebExchange mockExchange = mock(ServerWebExchange.class); + + + @Before + public void before() { + AgentBridge.agent = mockAgent; + when(mockAgent.getConfig()).thenReturn(mockConfig); + when(mockAgent.getLogger()).thenReturn(mockLogger); + when(mockLogger.isLoggable(Level.FINEST)).thenReturn(false); + } + + @After + public void after() { + AgentBridge.agent = originalAgent; + } + + // + // class_transformer.enhanced_spring_transaction_naming = false + // + @Test + public void handleInternal_findsAnnotationsFromInterfaceAndMethod_false() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(false); + InvocableHandlerMethod_Instrumentation cut = new InvocableHandlerMethod_InstrumentationTestImpl( + TestControllerClasses.ControllerClassWithInterface.class, + TestControllerClasses.ControllerClassWithInterface.class.getMethod("get")); + + ServerHttpRequest mockReq = mock(ServerHttpRequest.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockExchange.getRequest()).thenReturn(mockReq); + when(mockReq.getMethod()).thenReturn(HttpMethod.GET); + + //cut.handleInternal(mockReq, mockResp, handlerMethod); + cut.invoke(mockExchange, mock(BindingContext.class)); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/ControllerClassWithInterface/get"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "org.springframework.web.reactive.result.method.TestControllerClasses$ControllerClassWithInterface/get"); + } + + @Test + public void handleInternal_findsAnnotationsWithUrlParamFromInterfaceAndMethod_false() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(false); + InvocableHandlerMethod_Instrumentation cut = new InvocableHandlerMethod_InstrumentationTestImpl( + TestControllerClasses.ControllerClassWithInterface.class, + TestControllerClasses.ControllerClassWithInterface.class.getMethod("getParam")); + + ServerHttpRequest mockReq = mock(ServerHttpRequest.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockExchange.getRequest()).thenReturn(mockReq); + when(mockReq.getMethod()).thenReturn(HttpMethod.GET); + + //cut.handleInternal(mockReq, mockResp, handlerMethod); + cut.invoke(mockExchange, mock(BindingContext.class)); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/ControllerClassWithInterface/getParam"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "org.springframework.web.reactive.result.method.TestControllerClasses$ControllerClassWithInterface/getParam"); + } + + @Test + public void handleInternal_withRequestMappings_findsAnnotationsWithoutInterface_false() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(false); + InvocableHandlerMethod_Instrumentation cut = new InvocableHandlerMethod_InstrumentationTestImpl( + TestControllerClasses.StandardControllerWithAllRequestMappings.class, + TestControllerClasses.StandardControllerWithAllRequestMappings.class.getMethod("get")); + + ServerHttpRequest mockReq = mock(ServerHttpRequest.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockExchange.getRequest()).thenReturn(mockReq); + when(mockReq.getMethod()).thenReturn(HttpMethod.GET); + + //cut.handleInternal(mockReq, mockResp, handlerMethod); + cut.invoke(mockExchange, mock(BindingContext.class)); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/get (GET)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "org.springframework.web.reactive.result.method.TestControllerClasses$StandardControllerWithAllRequestMappings/get"); + } + + @Test + public void handleInternal_withRequestMappingsAndUrlParam_findsAnnotationsWithoutInterface_false() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(false); + InvocableHandlerMethod_Instrumentation cut = new InvocableHandlerMethod_InstrumentationTestImpl( + TestControllerClasses.StandardControllerWithAllRequestMappings.class, + TestControllerClasses.StandardControllerWithAllRequestMappings.class.getMethod("get2")); + + ServerHttpRequest mockReq = mock(ServerHttpRequest.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockExchange.getRequest()).thenReturn(mockReq); + when(mockReq.getMethod()).thenReturn(HttpMethod.GET); + + //cut.handleInternal(mockReq, mockResp, handlerMethod); + cut.invoke(mockExchange, mock(BindingContext.class)); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/get/{id} (GET)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "org.springframework.web.reactive.result.method.TestControllerClasses$StandardControllerWithAllRequestMappings/get2"); + } + + @Test + public void handleInternal_withPostMappings_findsAnnotationsWithoutInterface_false() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(false); + InvocableHandlerMethod_Instrumentation cut = new InvocableHandlerMethod_InstrumentationTestImpl( + TestControllerClasses.StandardControllerWithAllRequestMappings.class, + TestControllerClasses.StandardControllerWithAllRequestMappings.class.getMethod("post")); + + ServerHttpRequest mockReq = mock(ServerHttpRequest.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockExchange.getRequest()).thenReturn(mockReq); + when(mockReq.getMethod()).thenReturn(HttpMethod.POST); + + //cut.handleInternal(mockReq, mockResp, handlerMethod); + cut.invoke(mockExchange, mock(BindingContext.class)); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/post (POST)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "org.springframework.web.reactive.result.method.TestControllerClasses$StandardControllerWithAllRequestMappings/post"); + } + + @Test + public void handleInternal_whenNoAnnotationPresent_namesTxnBasedOnControllerClassAndMethod_false() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(false); + InvocableHandlerMethod_Instrumentation cut = new InvocableHandlerMethod_InstrumentationTestImpl( + TestControllerClasses.NoAnnotationController.class, + TestControllerClasses.NoAnnotationController.class.getMethod("get")); + + ServerHttpRequest mockReq = mock(ServerHttpRequest.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockExchange.getRequest()).thenReturn(mockReq); + when(mockReq.getMethod()).thenReturn(HttpMethod.GET); + + //cut.handleInternal(mockReq, mockResp, handlerMethod); + cut.invoke(mockExchange, mock(BindingContext.class)); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/NoAnnotationController/get"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "org.springframework.web.reactive.result.method.TestControllerClasses$NoAnnotationController/get"); + } + + @Test + public void handleInternal_whenExtendingAbstractController_namesTxnBasedOnRouteAndHttpMethod_false() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(false); + InvocableHandlerMethod_Instrumentation cut = new InvocableHandlerMethod_InstrumentationTestImpl( + TestControllerClasses.ControllerExtendingAbstractClass.class, + TestControllerClasses.ControllerExtendingAbstractClass.class.getMethod("extend")); + + ServerHttpRequest mockReq = mock(ServerHttpRequest.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockExchange.getRequest()).thenReturn(mockReq); + when(mockReq.getMethod()).thenReturn(HttpMethod.GET); + + //cut.handleInternal(mockReq, mockResp, handlerMethod); + cut.invoke(mockExchange, mock(BindingContext.class)); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/ControllerExtendingAbstractClass/extend"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "org.springframework.web.reactive.result.method.TestControllerClasses$ControllerExtendingAbstractClass/extend"); + } + + // + // class_transformer.enhanced_spring_transaction_naming = true + // + + @Test + public void handleInternal_findsAnnotationsFromInterfaceAndMethod_true() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(true); + InvocableHandlerMethod_Instrumentation cut = new InvocableHandlerMethod_InstrumentationTestImpl( + TestControllerClasses.ControllerClassWithInterface.class, + TestControllerClasses.ControllerClassWithInterface.class.getMethod("get")); + + ServerHttpRequest mockReq = mock(ServerHttpRequest.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockExchange.getRequest()).thenReturn(mockReq); + when(mockReq.getMethod()).thenReturn(HttpMethod.GET); + + //cut.handleInternal(mockReq, mockResp, handlerMethod); + cut.invoke(mockExchange, mock(BindingContext.class)); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/get (GET)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "org.springframework.web.reactive.result.method.TestControllerClasses$ControllerClassWithInterface/get"); + } + + @Test + public void handleInternal_findsAnnotationsWithUrlParamFromInterfaceAndMethod_true() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(true); + InvocableHandlerMethod_Instrumentation cut = new InvocableHandlerMethod_InstrumentationTestImpl( + TestControllerClasses.ControllerClassWithInterface.class, + TestControllerClasses.ControllerClassWithInterface.class.getMethod("getParam")); + + ServerHttpRequest mockReq = mock(ServerHttpRequest.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockExchange.getRequest()).thenReturn(mockReq); + when(mockReq.getMethod()).thenReturn(HttpMethod.GET); + + //cut.handleInternal(mockReq, mockResp, handlerMethod); + cut.invoke(mockExchange, mock(BindingContext.class)); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/get/{id} (GET)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "org.springframework.web.reactive.result.method.TestControllerClasses$ControllerClassWithInterface/getParam"); + } + + @Test + public void handleInternal_withRequestMappings_findsAnnotationsWithoutInterface_true() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(true); + InvocableHandlerMethod_Instrumentation cut = new InvocableHandlerMethod_InstrumentationTestImpl( + TestControllerClasses.StandardControllerWithAllRequestMappings.class, + TestControllerClasses.StandardControllerWithAllRequestMappings.class.getMethod("get")); + + ServerHttpRequest mockReq = mock(ServerHttpRequest.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockExchange.getRequest()).thenReturn(mockReq); + when(mockReq.getMethod()).thenReturn(HttpMethod.GET); + + //cut.handleInternal(mockReq, mockResp, handlerMethod); + cut.invoke(mockExchange, mock(BindingContext.class)); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/get (GET)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "org.springframework.web.reactive.result.method.TestControllerClasses$StandardControllerWithAllRequestMappings/get"); + } + + @Test + public void handleInternal_withRequestMappingsAndUrlParam_findsAnnotationsWithoutInterface_true() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(true); + InvocableHandlerMethod_Instrumentation cut = new InvocableHandlerMethod_InstrumentationTestImpl( + TestControllerClasses.StandardControllerWithAllRequestMappings.class, + TestControllerClasses.StandardControllerWithAllRequestMappings.class.getMethod("get2")); + + ServerHttpRequest mockReq = mock(ServerHttpRequest.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockExchange.getRequest()).thenReturn(mockReq); + when(mockReq.getMethod()).thenReturn(HttpMethod.GET); + + //cut.handleInternal(mockReq, mockResp, handlerMethod); + cut.invoke(mockExchange, mock(BindingContext.class)); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/get/{id} (GET)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "org.springframework.web.reactive.result.method.TestControllerClasses$StandardControllerWithAllRequestMappings/get2"); + } + + @Test + public void handleInternal_withPostMappings_findsAnnotationsWithoutInterface_true() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(true); + InvocableHandlerMethod_Instrumentation cut = new InvocableHandlerMethod_InstrumentationTestImpl( + TestControllerClasses.StandardControllerWithAllRequestMappings.class, + TestControllerClasses.StandardControllerWithAllRequestMappings.class.getMethod("post")); + + ServerHttpRequest mockReq = mock(ServerHttpRequest.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockExchange.getRequest()).thenReturn(mockReq); + when(mockReq.getMethod()).thenReturn(HttpMethod.POST); + + //cut.handleInternal(mockReq, mockResp, handlerMethod); + cut.invoke(mockExchange, mock(BindingContext.class)); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/post (POST)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "org.springframework.web.reactive.result.method.TestControllerClasses$StandardControllerWithAllRequestMappings/post"); + } + + @Test + public void handleInternal_whenNoAnnotationPresent_namesTxnBasedOnControllerClassAndMethod_true() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(true); + InvocableHandlerMethod_Instrumentation cut = new InvocableHandlerMethod_InstrumentationTestImpl( + TestControllerClasses.NoAnnotationController.class, + TestControllerClasses.NoAnnotationController.class.getMethod("get")); + + ServerHttpRequest mockReq = mock(ServerHttpRequest.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockExchange.getRequest()).thenReturn(mockReq); + when(mockReq.getMethod()).thenReturn(HttpMethod.GET); + + //cut.handleInternal(mockReq, mockResp, handlerMethod); + cut.invoke(mockExchange, mock(BindingContext.class)); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/NoAnnotationController/get"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "org.springframework.web.reactive.result.method.TestControllerClasses$NoAnnotationController/get"); + } + + @Test + public void handleInternal_whenExtendingAbstractController_namesTxnBasedOnRouteAndHttpMethod_true() throws Exception { + when(mockConfig.getValue("class_transformer.enhanced_spring_transaction_naming", false)).thenReturn(true); + InvocableHandlerMethod_Instrumentation cut = new InvocableHandlerMethod_InstrumentationTestImpl( + TestControllerClasses.ControllerExtendingAbstractClass.class, + TestControllerClasses.ControllerExtendingAbstractClass.class.getMethod("extend")); + + ServerHttpRequest mockReq = mock(ServerHttpRequest.class); + Transaction mockTxn = mock(Transaction.class); + + when(mockAgent.getTransaction(false)).thenReturn(mockTxn); + when(mockTxn.getTracedMethod()).thenReturn(mockTracedMethod); + when(mockExchange.getRequest()).thenReturn(mockReq); + when(mockReq.getMethod()).thenReturn(HttpMethod.GET); + + //cut.handleInternal(mockReq, mockResp, handlerMethod); + cut.invoke(mockExchange, mock(BindingContext.class)); + + verify(mockTxn).getTracedMethod(); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", "/root/extend (GET)"); + verify(mockTracedMethod).setMetricName("Spring", "Java", "org.springframework.web.reactive.result.method.TestControllerClasses$ControllerExtendingAbstractClass/extend"); + } +} diff --git a/instrumentation/spring-webflux-controller-mappings-5.0.0/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod_InstrumentationTestImpl.java b/instrumentation/spring-webflux-controller-mappings-5.0.0/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod_InstrumentationTestImpl.java new file mode 100644 index 0000000000..df0792fda0 --- /dev/null +++ b/instrumentation/spring-webflux-controller-mappings-5.0.0/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod_InstrumentationTestImpl.java @@ -0,0 +1,29 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package org.springframework.web.reactive.result.method; + +import java.lang.reflect.Method; + +public class InvocableHandlerMethod_InstrumentationTestImpl extends InvocableHandlerMethod_Instrumentation { + private final Class beanType; + private final Method bridgedMethod; + + public InvocableHandlerMethod_InstrumentationTestImpl(Class beanType, Method bridgedMethod) { + this.beanType = beanType; + this.bridgedMethod = bridgedMethod; + } + + @Override + protected Method getBridgedMethod() { + return bridgedMethod; + } + + @Override + public Class getBeanType() { + return beanType; + } +} diff --git a/instrumentation/spring-webflux-controller-mappings-5.0.0/src/test/java/org/springframework/web/reactive/result/method/SpringControllerUtilityOtherMethodsTest.java b/instrumentation/spring-webflux-controller-mappings-5.0.0/src/test/java/org/springframework/web/reactive/result/method/SpringControllerUtilityOtherMethodsTest.java new file mode 100644 index 0000000000..ac32c34d58 --- /dev/null +++ b/instrumentation/spring-webflux-controller-mappings-5.0.0/src/test/java/org/springframework/web/reactive/result/method/SpringControllerUtilityOtherMethodsTest.java @@ -0,0 +1,93 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package org.springframework.web.reactive.result.method; + +import com.newrelic.agent.bridge.Transaction; +import com.newrelic.agent.bridge.TransactionNamePriority; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class SpringControllerUtilityOtherMethodsTest { + @Test + public void retrieveRootMappingPathFromController_checkingInheritanceChain_returnsMapping() { + assertEquals("/root", + SpringControllerUtility.retrieveRootMappingPathFromController(TestControllerClasses.StandardController.class, true)); + + assertEquals("/root", + SpringControllerUtility.retrieveRootMappingPathFromController(TestControllerClasses.ControllerClassWithInterface.class, true)); + + assertEquals("/root", + SpringControllerUtility.retrieveRootMappingPathFromController(TestControllerClasses.ControllerExtendingAbstractClass.class, true)); + } + + @Test + public void retrieveRootMappingPathFromController_withoutCheckingInheritanceChain_returnsMappingWhenPresent() { + assertEquals("/root", + SpringControllerUtility.retrieveRootMappingPathFromController(TestControllerClasses.StandardController.class, false)); + + assertNull(SpringControllerUtility.retrieveRootMappingPathFromController(TestControllerClasses.ControllerClassWithInterface.class, false)); + + assertNull(SpringControllerUtility.retrieveRootMappingPathFromController(TestControllerClasses.ControllerExtendingAbstractClass.class, false)); + } + + @Test + public void doesClassContainControllerAnnotations_checkingInheritanceChain_returnsCorrectValue() { + assertTrue(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.StandardController.class, true)); + + assertTrue(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.ControllerClassWithInterface.class, true)); + + assertTrue(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.ControllerExtendingAbstractClass.class, true)); + + assertFalse(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.NoAnnotationController.class, true)); + } + + @Test + public void doesClassContainControllerAnnotations_withoutCheckingInheritanceChain_returnsCorrectValue() { + assertTrue(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.StandardController.class, false)); + + assertFalse(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.ControllerClassWithInterface.class, false)); + + assertFalse(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.ControllerExtendingAbstractClass.class, false)); + + assertFalse(SpringControllerUtility.doesClassContainControllerAnnotations(TestControllerClasses.NoAnnotationController.class, false)); + } + + @Test + public void assignTransactionNameFromControllerAndMethodRoutes_assignsProperName() { + Transaction mockTxn = mock(Transaction.class); + SpringControllerUtility.assignTransactionNameFromControllerAndMethodRoutes(mockTxn, "GET", "/root", "/get"); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", + "/root/get (GET)"); + } + + @Test + public void assignTransactionNameFromControllerAndMethod_assignsProperName() throws NoSuchMethodException { + Transaction mockTxn = mock(Transaction.class); + SpringControllerUtility.assignTransactionNameFromControllerAndMethod(mockTxn, TestControllerClasses.StandardController.class, + TestControllerClasses.StandardController.class.getMethod("get")); + verify(mockTxn).setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "SpringController", + "/StandardController/get"); + } + + @Test + public void getControllerClassAndMethodString_includingPrefix_returnsProperName() throws NoSuchMethodException { + assertEquals("org.springframework.web.reactive.result.method.TestControllerClasses$StandardController/get", SpringControllerUtility.getControllerClassAndMethodString(TestControllerClasses.StandardController.class, + TestControllerClasses.StandardController.class.getMethod("get"), true)); + } + + @Test + public void getControllerClassAndMethodString_notIncludingPrefix_returnsProperName() throws NoSuchMethodException { + assertEquals("StandardController/get", SpringControllerUtility.getControllerClassAndMethodString(TestControllerClasses.StandardController.class, + TestControllerClasses.StandardController.class.getMethod("get"), false)); + } +} diff --git a/instrumentation/spring-webflux-controller-mappings-5.0.0/src/test/java/org/springframework/web/reactive/result/method/SpringControllerUtilityRetrieveMappingTest.java b/instrumentation/spring-webflux-controller-mappings-5.0.0/src/test/java/org/springframework/web/reactive/result/method/SpringControllerUtilityRetrieveMappingTest.java new file mode 100644 index 0000000000..d958b9cc52 --- /dev/null +++ b/instrumentation/spring-webflux-controller-mappings-5.0.0/src/test/java/org/springframework/web/reactive/result/method/SpringControllerUtilityRetrieveMappingTest.java @@ -0,0 +1,75 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package org.springframework.web.reactive.result.method; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; + +@RunWith(value = Parameterized.class) +public class SpringControllerUtilityRetrieveMappingTest { + private final String httpMethod; + private final String expectedPath; + + @Parameterized.Parameters(name = "{index}: retrieveMappingPathFromHandlerMethod(httpMethod: {0}) == path value: {1}") + public static Collection data() { + return Arrays.asList(new Object[][]{ + {"GET", "/get"}, + {"PUT", "/put"}, + {"PATCH", "/patch"}, + {"POST", "/post"}, + {"DELETE", "/delete"} + }); + } + + public SpringControllerUtilityRetrieveMappingTest(String httpMethod, String expectedPath) { + this.httpMethod = httpMethod; + this.expectedPath = expectedPath; + } + + @Test + public void retrieveMappingPathFromHandlerMethod_withValidHttpMethodStrings() throws NoSuchMethodException { + Method method = TestControllerClasses.StandardController.class.getMethod(httpMethod.toLowerCase()); + String path = SpringControllerUtility.retrieveMappingPathFromHandlerMethod(method, httpMethod, true); + Assert.assertEquals(expectedPath, path); + } + + @Test + public void retrieveMappingPathFromHandlerMethod_withUnknownHttpMethod_returnsNullPath() throws NoSuchMethodException { + Method method = TestControllerClasses.StandardController.class.getMethod(this.httpMethod.toLowerCase()); + Assert.assertNull(SpringControllerUtility.retrieveMappingPathFromHandlerMethod(method, "Unknown", true)); + } + + @RequestMapping("/root") + public static class MyController { + @GetMapping("/get") + public void get() {} + + @PostMapping("/post") + public void post() {} + + @DeleteMapping("/delete") + public void delete() {} + + @PutMapping("/put") + public void put() {} + + @PatchMapping("/patch") + public void patch() {} + } +} diff --git a/instrumentation/spring-webflux-controller-mappings-5.0.0/src/test/java/org/springframework/web/reactive/result/method/TestControllerClasses.java b/instrumentation/spring-webflux-controller-mappings-5.0.0/src/test/java/org/springframework/web/reactive/result/method/TestControllerClasses.java new file mode 100644 index 0000000000..35fdab0506 --- /dev/null +++ b/instrumentation/spring-webflux-controller-mappings-5.0.0/src/test/java/org/springframework/web/reactive/result/method/TestControllerClasses.java @@ -0,0 +1,100 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package org.springframework.web.reactive.result.method; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +//Interfaces/classes used to test various mapping annotation scenarios +public class TestControllerClasses { + @RequestMapping(value = "/root") + @RestController + @Controller + public static class StandardController { + @GetMapping("/get") + public void get() {} + @PostMapping("/post") + public void post() {} + @DeleteMapping("/delete") + public void delete() {} + @PutMapping("/put") + public void put() {} + @PatchMapping("/patch") + public void patch() {} + } + + @RequestMapping(value = "/root") + @RestController + @Controller + public static class StandardControllerWithAllRequestMappings { + @RequestMapping("/get") + public void get() {} + @RequestMapping("/get/{id}") + public void get2() {} + @RequestMapping("/post") + public void post() {} + @RequestMapping("/delete") + public void delete() {} + @RequestMapping("/put") + public void put() {} + @RequestMapping("/patch") + public void patch() {} + } + + @RequestMapping("/root") + @RestController + @Controller + public interface ControllerInterface { + @GetMapping("/get") + void get(); + @PostMapping("/post") + void post(); + @DeleteMapping("delete") + void delete(); + @RequestMapping("/req") + void req(); + @GetMapping("/get/{id}") + void getParam(); + } + + public static class ControllerClassWithInterface implements ControllerInterface { + @Override + public void get() {} + @Override + public void post() {} + @Override + public void delete() {} + @Override + public void req() {} + @Override + public void getParam() {} + } + + @RequestMapping(value = "/root") + @RestController + @Controller + public static abstract class ControllerToExtend { + @GetMapping("/extend") + abstract public String extend(); + } + + public static class ControllerExtendingAbstractClass extends ControllerToExtend { + public String extend() { + return "extend"; + } + } + + public static class NoAnnotationController { + public void get() {} + } +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/config/ClassTransformerConfig.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/ClassTransformerConfig.java index 8450439b42..cbba83e37b 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/config/ClassTransformerConfig.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/ClassTransformerConfig.java @@ -106,4 +106,6 @@ public interface ClassTransformerConfig extends Config { boolean isWeavePackageEnabled(WeavePackageConfig weavePackageConfig); boolean isDefaultMethodTracingEnabled(); + + boolean isEnhancedSpringTransactionNaming(); } diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/config/ClassTransformerConfigImpl.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/ClassTransformerConfigImpl.java index 5fb62805f2..7d0848ee23 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/config/ClassTransformerConfigImpl.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/ClassTransformerConfigImpl.java @@ -45,6 +45,7 @@ final class ClassTransformerConfigImpl extends BaseConfig implements ClassTransf public static final String COMPUTE_FRAMES = "compute_frames"; public static final String SHUTDOWN_DELAY = "shutdown_delay"; public static final String GRANT_PACKAGE_ACCESS = "grant_package_access"; + public static final String ENHANCED_SPRING_TRANSACTION_NAMING = "enhanced_spring_transaction_naming"; public static final boolean DEFAULT_COMPUTE_FRAMES = true; public static final boolean DEFAULT_ENABLED = true; public static final boolean DEFAULT_DISABLED = false; @@ -53,6 +54,7 @@ final class ClassTransformerConfigImpl extends BaseConfig implements ClassTransf public static final int DEFAULT_MAX_PREVALIDATED_CLASSLOADERS = 10; public static final boolean DEFAULT_PREVALIDATE_WEAVE_PACKAGES = true; public static final boolean DEFAULT_PREMATCH_WEAVE_METHODS = true; + public static final boolean DEFAULT_ENHANCED_SPRING_TRANSACTION_NAMING = false; private static final String SYSTEM_PROPERTY_ROOT = "newrelic.config.class_transformer."; @@ -90,6 +92,7 @@ final class ClassTransformerConfigImpl extends BaseConfig implements ClassTransf private final int maxPreValidatedClassLoaders; private final boolean preValidateWeavePackages; private final boolean preMatchWeaveMethods; + private final boolean isEnhancedSpringTransactionNaming; private final AnnotationMatcher ignoreTransactionAnnotationMatcher; private final AnnotationMatcher ignoreApdexAnnotationMatcher; @@ -119,6 +122,7 @@ public ClassTransformerConfigImpl(Map props, boolean customTraci preMatchWeaveMethods = getProperty(PREMATCH_WEAVE_METHODS, DEFAULT_PREMATCH_WEAVE_METHODS); defaultMethodTracingEnabled = getProperty("default_method_tracing_enabled", true); autoAsyncLinkRateLimit = getProperty("auto_async_link_rate_limit", TimeUnit.SECONDS.toMillis(1)); + isEnhancedSpringTransactionNaming = getProperty(ENHANCED_SPRING_TRANSACTION_NAMING, DEFAULT_ENHANCED_SPRING_TRANSACTION_NAMING); this.traceAnnotationMatcher = customTracingEnabled ? initializeTraceAnnotationMatcher(props) : new NoMatchAnnotationMatcher(); this.ignoreTransactionAnnotationMatcher = new ClassNameAnnotationMatcher(AnnotationNames.NEW_RELIC_IGNORE_TRANSACTION, false); @@ -304,6 +308,11 @@ public long getAutoAsyncLinkRateLimit() { return autoAsyncLinkRateLimit; } + @Override + public boolean isEnhancedSpringTransactionNaming() { + return isEnhancedSpringTransactionNaming; + } + public static final String JDBC_STATEMENTS_PROPERTY = "jdbc_statements"; @Override diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/instrumentation/pointcuts/frameworks/spring/HandleInternalInvokerPointCut.java b/newrelic-agent/src/main/java/com/newrelic/agent/instrumentation/pointcuts/frameworks/spring/HandleInternalInvokerPointCut.java deleted file mode 100644 index 858b611b30..0000000000 --- a/newrelic-agent/src/main/java/com/newrelic/agent/instrumentation/pointcuts/frameworks/spring/HandleInternalInvokerPointCut.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * - * * Copyright 2020 New Relic Corporation. All rights reserved. - * * SPDX-License-Identifier: Apache-2.0 - * - */ - -package com.newrelic.agent.instrumentation.pointcuts.frameworks.spring; - -import java.util.logging.Level; - -import com.newrelic.agent.Agent; -import com.newrelic.agent.Transaction; -import com.newrelic.agent.instrumentation.PointCutClassTransformer; -import com.newrelic.agent.instrumentation.classmatchers.InterfaceMatcher; -import com.newrelic.agent.instrumentation.methodmatchers.OrMethodMatcher; -import com.newrelic.agent.instrumentation.pointcuts.PointCut; -import com.newrelic.agent.tracers.ClassMethodSignature; -import com.newrelic.agent.tracers.DefaultTracer; -import com.newrelic.agent.tracers.Tracer; -import com.newrelic.agent.tracers.metricname.SimpleMetricNameFormat; - -@PointCut -public class HandleInternalInvokerPointCut extends MethodInvokerPointCut { - - public HandleInternalInvokerPointCut(PointCutClassTransformer classTransformer) { - super( - new InterfaceMatcher("org/springframework/web/servlet/HandlerAdapter"), - OrMethodMatcher.getMethodMatcher( - createExactMethodMatcher( - "invokeHandleMethod", - "(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;Lorg/springframework/web/method/HandlerMethod;)Lorg/springframework/web/servlet/ModelAndView;"), - createExactMethodMatcher( - "invokeHandlerMethod", - "(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;Lorg/springframework/web/method/HandlerMethod;)Lorg/springframework/web/servlet/ModelAndView;"), - createExactMethodMatcher( - "invokeHandleMethod", - "(Ljakarta/servlet/http/HttpServletRequest;Ljakarta/servlet/http/HttpServletResponse;Lorg/springframework/web/method/HandlerMethod;)Lorg/springframework/web/servlet/ModelAndView;"), - createExactMethodMatcher( - "invokeHandlerMethod", - "(Ljakarta/servlet/http/HttpServletRequest;Ljakarta/servlet/http/HttpServletResponse;Lorg/springframework/web/method/HandlerMethod;)Lorg/springframework/web/servlet/ModelAndView;"))); - } - - @Override - public Tracer doGetTracer(final Transaction transaction, ClassMethodSignature sig, Object invoker, - final Object[] args) { - - String methodName = null; - Class controller = null; - StringBuilder tracerName = new StringBuilder("Spring/Java"); - - // grab the information we need - try { - HandlerMethod methodInfo = (HandlerMethod) args[2]; - - methodName = methodInfo._nr_getBridgedMethod().getName(); - controller = methodInfo._nr_getBean().getClass(); - // build the tracer name; - tracerName.append(getControllerName(methodName, controller)); - - setTransactionName(transaction, methodName, controller); - - } catch (Exception e) { - // if it fails use the method name - Agent.LOG.log(Level.FINE, "Unable to pull controller and method from spring framework."); - Agent.LOG.log(Level.FINEST, "Exception grabbing spring controller.", e); - tracerName.append(sig.getMethodName()); - } - - return new DefaultTracer(transaction, sig, invoker, new SimpleMetricNameFormat(tracerName.toString())); - } - - private String getControllerName(String methodName, Class controller) { - String controllerName = controller.getName(); - int indexOf = controllerName.indexOf(TO_REMOVE); - if (indexOf > 0) { - controllerName = controllerName.substring(0, indexOf); - } - return '/' + controllerName + '/' + methodName; - } -} diff --git a/newrelic-agent/src/main/resources/newrelic.yml b/newrelic-agent/src/main/resources/newrelic.yml index 14ed8a4c9a..f8eda0e5c6 100644 --- a/newrelic-agent/src/main/resources/newrelic.yml +++ b/newrelic-agent/src/main/resources/newrelic.yml @@ -360,6 +360,17 @@ common: &default_settings org.mvel2.optimizers.impl.asm.ASMAccessorOptimizer$ContextClassLoader, gw.internal.gosu.compiler.SingleServingGosuClassLoader, + # Enhanced Spring transaction naming. + # This feature will name any transaction that originates from a Spring controller after + # the defined route and HTTP method. For example: "/customer/v1/edit (POST)". + # This includes controllers that implement or extend interfaces/classes with WebMVC related + # annotations (@RestController, @Controller, @RequestMapping, etc). By default, this is configured + # to false, which will name transactions for those types of controllers based on the controller + # class name and method. For example; "CustomerController/edit". This is the naming logic carried + # over from previous agent versions. "Standard" controllers, with all relevant annotations + # present on the actual class, will still get named based on route and HTTP method. + enhanced_spring_transaction_naming: false + # Real-time profiling using Java Flight Recorder (JFR). # This feature reports dimensional metrics to the ingest endpoint configured by # metric_ingest_uri and events to the ingest endpoint configured by event_ingest_uri. diff --git a/settings.gradle b/settings.gradle index 848313f152..34e63decf7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -330,6 +330,7 @@ include 'instrumentation:spring-3.0.0' include 'instrumentation:spring-4.0.0' include 'instrumentation:spring-4.2.0' include 'instrumentation:spring-4.3.0' +include 'instrumentation:spring-6.0.0' include 'instrumentation:spring-aop-2' include 'instrumentation:spring-cache-3.1.0' include 'instrumentation:spring-jms-2' @@ -340,6 +341,7 @@ include 'instrumentation:spring-webflux-6.0.0' include 'instrumentation:spring-webflux-5.0.0' include 'instrumentation:spring-webflux-5.1.0' include 'instrumentation:spring-webflux-5.3.0' +include 'instrumentation:spring-webflux-controller-mappings-5.0.0' include 'instrumentation:spring-ws-2.0' include 'instrumentation:spymemcached-2.12.0' include 'instrumentation:sttp-2.12_2.2.3'