Skip to content

Commit

Permalink
Merge pull request #2077 from newrelic/actuator-module
Browse files Browse the repository at this point in the history
Actuator endpoint naming for Spring Boot 3
  • Loading branch information
jtduffy authored Sep 30, 2024
2 parents 0708427 + caa721f commit bdfa0f4
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 0 deletions.
12 changes: 12 additions & 0 deletions instrumentation/spring-boot-actuator-3.0.0/README.md
Original file line number Diff line number Diff line change
@@ -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.
37 changes: 37 additions & 0 deletions instrumentation/spring-boot-actuator-3.0.0/build.gradle
Original file line number Diff line number Diff line change
@@ -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'
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> 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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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"));
}
}
8 changes: 8 additions & 0 deletions newrelic-agent/src/main/resources/newrelic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down

0 comments on commit bdfa0f4

Please sign in to comment.