Skip to content

Commit bad3690

Browse files
authored
Drop the duplicate handler resolution in Spring Web 6 instrumentation (#11533)
Drop the duplicate handler resolution in Spring Web 6 instrumentation reintroduce the old instrumentation and make it opt-out Co-authored-by: andrea.marziali <andrea.marziali@datadoghq.com>
1 parent 1b95bfa commit bad3690

11 files changed

Lines changed: 196 additions & 43 deletions

File tree

dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/src/main/java/datadog/trace/instrumentation/springweb6/DispatcherServletInstrumentation.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
55
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
66
import static net.bytebuddy.matcher.ElementMatchers.isProtected;
7+
import static net.bytebuddy.matcher.ElementMatchers.returns;
78
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
89
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
910

@@ -27,10 +28,7 @@ public String instrumentedType() {
2728
@Override
2829
public String[] helperClassNames() {
2930
return new String[] {
30-
packageName + ".SpringWebHttpServerDecorator",
31-
packageName + ".ServletRequestURIAdapter",
32-
packageName + ".HandlerMappingResourceNameFilter",
33-
packageName + ".PathMatchingHttpServletRequestWrapper",
31+
packageName + ".SpringWebHttpServerDecorator", packageName + ".ServletRequestURIAdapter",
3432
};
3533
}
3634

@@ -39,9 +37,10 @@ public void methodAdvice(MethodTransformer transformer) {
3937
transformer.applyAdvice(
4038
isMethod()
4139
.and(isProtected())
42-
.and(named("onRefresh"))
43-
.and(takesArgument(0, named("org.springframework.context.ApplicationContext")))
44-
.and(takesArguments(1)),
40+
.and(named("getHandler"))
41+
.and(takesArgument(0, named("jakarta.servlet.http.HttpServletRequest")))
42+
.and(takesArguments(1))
43+
.and(returns(named("org.springframework.web.servlet.HandlerExecutionChain"))),
4544
packageName + ".HandlerMappingAdvice");
4645
transformer.applyAdvice(
4746
isMethod()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package datadog.trace.instrumentation.springweb6;
2+
3+
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
4+
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
5+
import static net.bytebuddy.matcher.ElementMatchers.isProtected;
6+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
7+
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
8+
9+
import com.google.auto.service.AutoService;
10+
import datadog.trace.agent.tooling.Instrumenter;
11+
import datadog.trace.agent.tooling.InstrumenterModule;
12+
13+
/**
14+
* When the opt-in {@code spring-path-filter} integration is enabled, wires the {@code
15+
* DispatcherServlet} handler mappings into {@link HandlerMappingResourceNameFilter} on context
16+
* refresh. Disabled by default; the default route naming is handled by {@link HandlerMappingAdvice}
17+
* ({@code getHandler}) and {@link ControllerAdvice}.
18+
*/
19+
@AutoService(InstrumenterModule.class)
20+
public final class ResourceNameFilterMappingInstrumentation extends InstrumenterModule.Tracing
21+
implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {
22+
23+
public ResourceNameFilterMappingInstrumentation() {
24+
super("spring-web", "spring-path-filter");
25+
}
26+
27+
@Override
28+
protected boolean defaultEnabled() {
29+
return false;
30+
}
31+
32+
@Override
33+
public String instrumentedType() {
34+
return "org.springframework.web.servlet.DispatcherServlet";
35+
}
36+
37+
@Override
38+
public String[] helperClassNames() {
39+
return new String[] {
40+
packageName + ".SpringWebHttpServerDecorator",
41+
packageName + ".ServletRequestURIAdapter",
42+
packageName + ".HandlerMappingResourceNameFilter",
43+
packageName + ".HandlerMappingResourceNameFilter$BeanDefinition",
44+
packageName + ".PathMatchingHttpServletRequestWrapper",
45+
};
46+
}
47+
48+
@Override
49+
public void methodAdvice(MethodTransformer transformer) {
50+
transformer.applyAdvice(
51+
isMethod()
52+
.and(isProtected())
53+
.and(named("onRefresh"))
54+
.and(takesArgument(0, named("org.springframework.context.ApplicationContext")))
55+
.and(takesArguments(1)),
56+
packageName + ".ResourceNameFilterMappingAdvice");
57+
}
58+
}

dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/src/main/java/datadog/trace/instrumentation/springweb6/ServletPathRequestFilterInstrumentation.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
import net.bytebuddy.matcher.ElementMatcher;
1515

1616
/**
17-
* This instrumentation adds the ServletPathRequestFilter definition to the spring context When the
18-
* context is created, the filter will be added to the beginning of the filter chain
17+
* Companion to {@link WebApplicationContextInstrumentation}: when the opt-in {@code
18+
* spring-path-filter} integration is enabled, registers Spring's {@code ServletRequestPathFilter}
19+
* first in the chain so the request path is parsed before {@link HandlerMappingResourceNameFilter}
20+
* resolves the handler. Disabled by default.
1921
*/
2022
@AutoService(InstrumenterModule.class)
2123
public class ServletPathRequestFilterInstrumentation extends InstrumenterModule.Tracing
@@ -24,6 +26,11 @@ public ServletPathRequestFilterInstrumentation() {
2426
super("spring-web", "spring-path-filter");
2527
}
2628

29+
@Override
30+
protected boolean defaultEnabled() {
31+
return false;
32+
}
33+
2734
@Override
2835
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
2936
return hasClassNamed("org.springframework.web.filter.ServletRequestPathFilter")

dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/src/main/java/datadog/trace/instrumentation/springweb6/WebApplicationContextInstrumentation.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
import net.bytebuddy.matcher.ElementMatcher;
1414

1515
/**
16-
* This instrumentation adds the HandlerMappingResourceNameFilter definition to the spring context
17-
* When the context is created, the filter will be added to the beginning of the filter chain
16+
* Opt-in alternative to the default {@code getHandler}/{@code ControllerAdvice} route naming. Adds
17+
* the {@link HandlerMappingResourceNameFilter} bean to the Spring context, which names the route at
18+
* the start of the filter chain by resolving the handler itself. This is the legacy approach and is
19+
* disabled by default; enable it with {@code dd.integration.spring-path-filter.enabled=true}.
1820
*/
1921
@AutoService(InstrumenterModule.class)
2022
public class WebApplicationContextInstrumentation extends InstrumenterModule.Tracing
@@ -23,6 +25,11 @@ public WebApplicationContextInstrumentation() {
2325
super("spring-web", "spring-path-filter");
2426
}
2527

28+
@Override
29+
protected boolean defaultEnabled() {
30+
return false;
31+
}
32+
2633
@Override
2734
public String hierarchyMarkerType() {
2835
return "org.springframework.web.context.WebApplicationContext";

dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/src/main/java17/datadog/trace/instrumentation/springweb6/ControllerAdvice.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ public static ContextScope nameResourceAndStartSpan(
2525
@Advice.Argument(2) final Object handler,
2626
@Advice.Local("handlerSpanKey") String handlerSpanKey) {
2727
handlerSpanKey = "";
28-
// Name the parent span based on the matching pattern
28+
29+
/*
30+
By the time HandlerAdapter.handle runs, every handler mapping kind (annotated and SimpleUrlHandlerMapping via its
31+
PathExposingHandlerInterceptor) has populated BEST_MATCHING_PATTERN_ATTRIBUTE.
32+
*/
2933
Object contextObj = request.getAttribute(DD_CONTEXT_ATTRIBUTE);
3034
if (contextObj instanceof Context) {
3135
Context context = (Context) contextObj;

dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/src/main/java17/datadog/trace/instrumentation/springweb6/HandleMatchAdvice.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ public static void after(
3737
return;
3838
}
3939

40-
// hacky, but APM instrumentation causes the instrumented method to be called twice
40+
// When the opt-in spring-path-filter integration is enabled, the filter resolves the handler
41+
// against a PathMatchingHttpServletRequestWrapper, which triggers handleMatch a second time.
4142
if (req.getClass()
4243
.getName()
4344
.equals("datadog.trace.instrumentation.springweb6.PathMatchingHttpServletRequestWrapper")) {

dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/src/main/java17/datadog/trace/instrumentation/springweb6/HandlerMappingAdvice.java

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,39 @@
11
package datadog.trace.instrumentation.springweb6;
22

3-
import java.util.List;
3+
import static datadog.context.Context.root;
4+
import static datadog.trace.bootstrap.instrumentation.api.Java8BytecodeBridge.spanFromContext;
5+
import static datadog.trace.bootstrap.instrumentation.decorator.HttpServerDecorator.DD_CONTEXT_ATTRIBUTE;
6+
import static datadog.trace.instrumentation.springweb6.SpringWebHttpServerDecorator.DECORATE;
7+
8+
import datadog.context.Context;
9+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
10+
import jakarta.servlet.http.HttpServletRequest;
411
import net.bytebuddy.asm.Advice;
5-
import org.springframework.context.ApplicationContext;
6-
import org.springframework.web.servlet.HandlerMapping;
12+
import org.springframework.web.servlet.HandlerExecutionChain;
713

814
/**
9-
* This advice creates a filter that has reference to the handlerMappings from DispatcherServlet
10-
* which allows the mappings to be evaluated at the beginning of the filter chain. This evaluation
11-
* is done inside the Servlet3Decorator.onContext method.
15+
* Names the server span route as soon as {@code DispatcherServlet} has resolved the handler.
16+
*
17+
* <p>For {@code RequestMappingInfoHandlerMapping}, {@code handleMatch} sets {@link
18+
* org.springframework.web.servlet.HandlerMapping#BEST_MATCHING_PATTERN_ATTRIBUTE} synchronously
19+
* inside {@code getHandler}, before interceptors run. Naming the route here means it survives a
20+
* {@code HandlerInterceptor.preHandle} that aborts the request before the controller executes.
1221
*/
1322
public class HandlerMappingAdvice {
1423

15-
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
16-
public static void afterRefresh(
17-
@Advice.Argument(0) final ApplicationContext springCtx,
18-
@Advice.FieldValue("handlerMappings") final List<HandlerMapping> handlerMappings) {
19-
if (springCtx.containsBean("ddDispatcherFilter")) {
20-
final datadog.trace.instrumentation.springweb6.HandlerMappingResourceNameFilter filter =
21-
(HandlerMappingResourceNameFilter) springCtx.getBean("ddDispatcherFilter");
22-
if (handlerMappings != null && filter != null) {
23-
filter.setHandlerMappings(handlerMappings);
24+
@Advice.OnMethodExit(suppress = Throwable.class)
25+
public static void onExit(
26+
@Advice.Argument(0) final HttpServletRequest request,
27+
@Advice.Return final HandlerExecutionChain chain) {
28+
if (chain == null) {
29+
// No handler matched (e.g. 404): leave the resource name untouched.
30+
return;
31+
}
32+
final Object contextObj = request.getAttribute(DD_CONTEXT_ATTRIBUTE);
33+
if (contextObj instanceof Context) {
34+
final AgentSpan parentSpan = spanFromContext((Context) contextObj);
35+
if (parentSpan != null) {
36+
DECORATE.onRequest(parentSpan, request, request, root());
2437
}
2538
}
2639
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package datadog.trace.instrumentation.springweb6;
2+
3+
import java.util.List;
4+
import net.bytebuddy.asm.Advice;
5+
import org.springframework.context.ApplicationContext;
6+
import org.springframework.web.servlet.HandlerMapping;
7+
8+
/**
9+
* Wires the {@code DispatcherServlet} handler mappings into the (opt-in) {@link
10+
* HandlerMappingResourceNameFilter} after the context refreshes, so the filter can evaluate them at
11+
* the start of the filter chain. Only active when the {@code spring-path-filter} integration is
12+
* enabled (see {@code ResourceNameFilterMappingInstrumentation}).
13+
*/
14+
public class ResourceNameFilterMappingAdvice {
15+
16+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
17+
public static void afterRefresh(
18+
@Advice.Argument(0) final ApplicationContext springCtx,
19+
@Advice.FieldValue("handlerMappings") final List<HandlerMapping> handlerMappings) {
20+
if (handlerMappings != null && springCtx.containsBean("ddDispatcherFilter")) {
21+
final HandlerMappingResourceNameFilter filter =
22+
(HandlerMappingResourceNameFilter) springCtx.getBean("ddDispatcherFilter");
23+
filter.setHandlerMappings(handlerMappings);
24+
}
25+
}
26+
}

dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/src/test/groovy/datadog/trace/instrumentation/springweb6/boot/SpringBootBasedTest.groovy

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ import datadog.trace.bootstrap.instrumentation.api.Tags
1616
import datadog.trace.core.DDSpan
1717
import datadog.trace.instrumentation.springweb6.SetupSpecHelper
1818
import datadog.trace.instrumentation.springweb6.SpringWebHttpServerDecorator
19-
import jakarta.servlet.http.HttpServletRequest
20-
import jakarta.servlet.http.HttpServletResponse
2119
import okhttp3.Credentials
2220
import okhttp3.FormBody
2321
import okhttp3.Request
@@ -28,7 +26,6 @@ import org.springframework.boot.SpringApplication
2826
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext
2927
import org.springframework.context.ConfigurableApplicationContext
3028
import org.springframework.http.MediaType
31-
import org.springframework.web.servlet.HandlerInterceptor
3229
import org.springframework.web.socket.BinaryMessage
3330
import org.springframework.web.socket.TextMessage
3431
import org.springframework.web.socket.WebSocketSession
@@ -59,7 +56,7 @@ class SpringBootBasedTest extends HttpServerTest<ConfigurableApplicationContext>
5956
Map<String, String> extraServerTags = [:]
6057

6158
SpringApplication application() {
62-
return new SpringApplication(SecurityConfig, TestController, AppConfig, WebsocketConfig)
59+
return new SpringApplication(SecurityConfig, TestController, AppConfig, WebsocketConfig, FailOnHeaderConfig)
6360
}
6461

6562
class SpringBootServer implements WebsocketServer {
@@ -451,26 +448,20 @@ class SpringBootBasedTest extends HttpServerTest<ConfigurableApplicationContext>
451448
InstrumentationBridge.clearIastModules()
452449
}
453450

454-
def 'path is extract when preHandle fails'() {
451+
def 'path is extracted when preHandle fails'() {
455452
setup:
456453
def request = request(PATH_PARAM, 'GET', null).header("fail", "true").build()
457-
context.getBeanFactory().registerSingleton("testHandler", new HandlerInterceptor() {
458-
@Override
459-
boolean preHandle(HttpServletRequest req, HttpServletResponse response, Object handler) throws Exception {
460-
if ("true".equalsIgnoreCase(req.getHeader("fail"))) {
461-
throw new RuntimeException("Stop here")
462-
}
463-
return true
464-
}
465-
})
466454

467455
when:
468-
client.newCall(request).execute()
456+
def response = client.newCall(request).execute()
457+
response.close()
469458
TEST_WRITER.waitForTraces(1)
470459
DDSpan span = TEST_WRITER.flatten().find { "servlet.request".contentEquals(it.operationName) }
471460

472461
then:
462+
response.code() == 500
473463
span.getResourceName().toString() == "GET " + testPathParam()
464+
span.isError()
474465
}
475466

476467
boolean hasResponseSpan(ServerEndpoint endpoint) {
@@ -526,3 +517,16 @@ class SpringBootRumInjectionForkedTest extends SpringBootBasedTest {
526517
true
527518
}
528519
}
520+
521+
/**
522+
* Runs the full suite with the opt-in legacy route-naming filter enabled, to verify that path still
523+
* produces the same traces (including APMS-8174). The default path (getHandler + ControllerAdvice)
524+
* stays active; the DD_FILTERED_SPRING_ROUTE_ALREADY_APPLIED guard prevents double naming.
525+
*/
526+
class SpringBootResourceNameFilterForkedTest extends SpringBootBasedTest {
527+
@Override
528+
protected void configurePreAgent() {
529+
super.configurePreAgent()
530+
injectSysConfig("dd.integration.spring-path-filter.enabled", "true")
531+
}
532+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package datadog.trace.instrumentation.springweb6.boot;
2+
3+
import org.springframework.context.annotation.Configuration;
4+
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
5+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
6+
7+
@Configuration
8+
public class FailOnHeaderConfig implements WebMvcConfigurer {
9+
@Override
10+
public void addInterceptors(final InterceptorRegistry registry) {
11+
registry.addInterceptor(new FailOnHeaderInterceptor());
12+
}
13+
}

0 commit comments

Comments
 (0)