diff --git a/instrumentation/spring-boot-actuator-3.0.0/README.md b/instrumentation/spring-boot-actuator-3.0.0/README.md new file mode 100644 index 0000000000..61d92ff23c --- /dev/null +++ b/instrumentation/spring-boot-actuator-3.0.0/README.md @@ -0,0 +1,12 @@ +# spring-actuator-3.0.0 Instrumentation Module + +By default, built-in actuator endpoints and custom actuator endpoints (using the @Endpoint annotation +and it's subclasses) will all be named as "OperationHandler/handle" in New Relic. Activating this +module will result in the transaction name reflecting the actual base actuator endpoint URI. +For example, invoking "/actuator/loggers" or "actuator/loggers/com.newrelic" will result in the +transaction name "actuator/loggers (GET)". This is to prevent MGI. + +To activate actuator naming, set the following configuration to true: +- `class_transformer.name_actuator_endpoints` + +The default value is false. \ No newline at end of file diff --git a/instrumentation/spring-boot-actuator-3.0.0/build.gradle b/instrumentation/spring-boot-actuator-3.0.0/build.gradle new file mode 100644 index 0000000000..e3c7b2fee2 --- /dev/null +++ b/instrumentation/spring-boot-actuator-3.0.0/build.gradle @@ -0,0 +1,37 @@ +plugins { + id "org.jetbrains.kotlin.jvm" +} + +dependencies { + implementation(project(":agent-bridge")) + implementation("org.springframework.boot:spring-boot-actuator:3.0.0") + implementation('jakarta.servlet:jakarta.servlet-api:5.0.0') +} + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.spring-boot-actuator-3.0.0' } +} + +verifyInstrumentation { + passesOnly('org.springframework.boot:spring-boot-actuator:[3.0.0,)') { + implementation('jakarta.servlet:jakarta.servlet-api:5.0.0') + } +} + +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-boot-actuator-3.0.0/src/main/java/com/nr/agent/instrumentation/actuator/SpringActuatorUtils.java b/instrumentation/spring-boot-actuator-3.0.0/src/main/java/com/nr/agent/instrumentation/actuator/SpringActuatorUtils.java new file mode 100644 index 0000000000..de96047d49 --- /dev/null +++ b/instrumentation/spring-boot-actuator-3.0.0/src/main/java/com/nr/agent/instrumentation/actuator/SpringActuatorUtils.java @@ -0,0 +1,32 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.nr.agent.instrumentation.actuator; + +import com.newrelic.api.agent.NewRelic; +import org.springframework.util.StringUtils; + +import java.util.List; + +public class SpringActuatorUtils { + public static final boolean isActuatorEndpointNamingEnabled = NewRelic.getAgent().getConfig().getValue("class_transformer.name_actuator_endpoints", false); + + public static String normalizeActuatorUri(String uri) { + String modifiedUri = null; + if (uri != null && uri.length() > 1) { + // Normalize the uri by removing the leading "/" and stripping and path components + // other than the first two, to prevent MGI for certain actuator endpoints. + // For example, "/actuator/loggers/com.newrelic" will be converted into + // "actuator/loggers" + String [] parts = uri.substring(uri.charAt(0) == '/' ? 1 : 0).split("/"); + if (parts.length >= 2) { + modifiedUri = parts[0] + "/" + parts[1]; + } + } + + return modifiedUri; + } +} diff --git a/instrumentation/spring-boot-actuator-3.0.0/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping_Instrumentation.java b/instrumentation/spring-boot-actuator-3.0.0/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping_Instrumentation.java new file mode 100644 index 0000000000..a8988dc05f --- /dev/null +++ b/instrumentation/spring-boot-actuator-3.0.0/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping_Instrumentation.java @@ -0,0 +1,44 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package org.springframework.boot.actuate.endpoint.web.servlet; + +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.Transaction; +import com.newrelic.api.agent.TransactionNamePriority; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import com.nr.agent.instrumentation.actuator.SpringActuatorUtils; +import jakarta.servlet.http.HttpServletRequest; + +import java.util.Map; + +@Weave(type = MatchType.BaseClass, originalName = "org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping") +public class AbstractWebMvcEndpointHandlerMapping_Instrumentation { + @Weave(type = MatchType.ExactClass, originalName = "org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler") + private static final class OperationHandler_Instrumentation { + @Trace + Object handle(HttpServletRequest request, Map body) { + if (SpringActuatorUtils.isActuatorEndpointNamingEnabled) { + Transaction transaction = AgentBridge.getAgent().getTransaction(false); + + if (transaction != null) { + String reportablePrefix = SpringActuatorUtils.normalizeActuatorUri(request.getRequestURI()); + + if (reportablePrefix != null) { + transaction.setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, true, "Spring", + reportablePrefix + " (" + request.getMethod() + ")"); + } + } + } + + return Weaver.callOriginal(); + } + } +} diff --git a/instrumentation/spring-boot-actuator-3.0.0/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/SpringActuatorUtilsTest.java b/instrumentation/spring-boot-actuator-3.0.0/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/SpringActuatorUtilsTest.java new file mode 100644 index 0000000000..f8c1eb1123 --- /dev/null +++ b/instrumentation/spring-boot-actuator-3.0.0/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/SpringActuatorUtilsTest.java @@ -0,0 +1,30 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package org.springframework.boot.actuate.endpoint.web.servlet; + +import com.nr.agent.instrumentation.actuator.SpringActuatorUtils; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class SpringActuatorUtilsTest { + @Test + public void normalizeActuatorUri_withNullValue_returnsNull() { + assertNull(SpringActuatorUtils.normalizeActuatorUri(null)); + } + + @Test + public void normalizeActuatorUri_withEmptyString_returnsNull() { + assertNull(SpringActuatorUtils.normalizeActuatorUri("")); + } + + @Test + public void normalizeActuatorUri_withValidUri_returnsModifiedUri() { + assertEquals("actuator/health", SpringActuatorUtils.normalizeActuatorUri("/actuator/health")); + assertEquals("actuator/health", SpringActuatorUtils.normalizeActuatorUri("actuator/health")); + } +} diff --git a/newrelic-agent/src/main/resources/newrelic.yml b/newrelic-agent/src/main/resources/newrelic.yml index 845597dd5a..cf5698cdcd 100644 --- a/newrelic-agent/src/main/resources/newrelic.yml +++ b/newrelic-agent/src/main/resources/newrelic.yml @@ -394,6 +394,14 @@ common: &default_settings # present on the actual class, will still get named based on route and HTTP method. enhanced_spring_transaction_naming: false + # By default, built-in actuator endpoints and custom actuator endpoints (using the @Endpoint annotation + # and it's subclasses) will all be named as "OperationHandler/handle" in New Relic. Setting this + # to true will result in the transaction name reflecting the actual base actuator endpoint URI. + # For example, invoking "/actuator/loggers" or "actuator/loggers/com.newrelic" will result in the + # transaction name "actuator/loggers (GET)". This is to prevent MGI. + # Default is false. + name_actuator_endpoints: 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 57a9c25fa1..dbfade3238 100644 --- a/settings.gradle +++ b/settings.gradle @@ -364,6 +364,7 @@ include 'instrumentation:spring-6.0.0' include 'instrumentation:spring-aop-2' include 'instrumentation:spring-batch-4.0.0' include 'instrumentation:spring-batch-5.0.0' +include 'instrumentation:spring-boot-actuator-3.0.0' include 'instrumentation:spring-cache-3.1.0' include 'instrumentation:spring-jms-2' include 'instrumentation:spring-jms-3'