Skip to content

Commit 86e5bec

Browse files
Add support for endpoint discovery in spring mvc (#8352)
Add support for endpoint discovery in spring mvc
1 parent 4f278de commit 86e5bec

File tree

34 files changed

+1117
-12
lines changed

34 files changed

+1117
-12
lines changed

.circleci/config.continue.yml.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ instrumentation_modules: &instrumentation_modules "dd-java-agent/instrumentation
3636
debugger_modules: &debugger_modules "dd-java-agent/agent-debugger|dd-java-agent/agent-bootstrap|dd-java-agent/agent-builder|internal-api|communication|dd-trace-core"
3737
profiling_modules: &profiling_modules "dd-java-agent/agent-profiling"
3838

39-
default_system_tests_commit: &default_system_tests_commit 1ef00a34ad1f83ae999887e510ef1ea1c27b151b
39+
default_system_tests_commit: &default_system_tests_commit 9be8b2793d687c7d9b39f3265fef27b5ec91910c
4040

4141
parameters:
4242
nightly:

dd-java-agent/instrumentation/spring-webmvc-3.1/build.gradle

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ muzzle {
3434
extraDependency "javax.servlet:javax.servlet-api:3.0.1"
3535
extraDependency "org.springframework:spring-webmvc:3.1.0.RELEASE"
3636
}
37+
38+
pass {
39+
name = 'spring-mvc-pre-5.3'
40+
group = 'org.springframework'
41+
module = 'spring-webmvc'
42+
versions = "[3.1.0.RELEASE,5.3)"
43+
skipVersions += [
44+
'1.2.1',
45+
'1.2.2',
46+
'1.2.3',
47+
'1.2.4'] // broken releases... missing dependencies
48+
skipVersions += '3.2.1.RELEASE' // missing a required class. (bad release?)
49+
extraDependency "javax.servlet:javax.servlet-api:3.0.1"
50+
}
3751
}
3852

3953
apply from: "$rootDir/gradle/java.gradle"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package datadog.trace.instrumentation.springweb;
2+
3+
import static datadog.trace.agent.tooling.bytebuddy.matcher.ClassLoaderMatchers.hasClassNamed;
4+
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
5+
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
6+
import static net.bytebuddy.matcher.ElementMatchers.isProtected;
7+
import static net.bytebuddy.matcher.ElementMatchers.not;
8+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
9+
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
10+
11+
import com.google.auto.service.AutoService;
12+
import datadog.trace.agent.tooling.Instrumenter;
13+
import datadog.trace.agent.tooling.InstrumenterModule;
14+
import datadog.trace.api.Config;
15+
import datadog.trace.api.telemetry.EndpointCollector;
16+
import java.util.Map;
17+
import net.bytebuddy.asm.Advice;
18+
import net.bytebuddy.matcher.ElementMatcher;
19+
import org.springframework.context.ApplicationContext;
20+
import org.springframework.web.method.HandlerMethod;
21+
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
22+
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
23+
24+
@AutoService(InstrumenterModule.class)
25+
public class AppSecDispatcherServletInstrumentation extends InstrumenterModule.AppSec
26+
implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {
27+
28+
public AppSecDispatcherServletInstrumentation() {
29+
super("spring-web");
30+
}
31+
32+
@Override
33+
public String instrumentedType() {
34+
return "org.springframework.web.servlet.DispatcherServlet";
35+
}
36+
37+
@Override
38+
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
39+
return not(
40+
hasClassNamed(
41+
"org.springframework.web.servlet.mvc.condition.PathPatternsRequestCondition"));
42+
}
43+
44+
@Override
45+
public String muzzleDirective() {
46+
return "spring-mvc-pre-5.3";
47+
}
48+
49+
@Override
50+
public String[] helperClassNames() {
51+
return new String[] {packageName + ".RequestMappingInfoIterator"};
52+
}
53+
54+
@Override
55+
public void methodAdvice(MethodTransformer transformer) {
56+
transformer.applyAdvice(
57+
isMethod()
58+
.and(isProtected())
59+
.and(named("onRefresh"))
60+
.and(takesArgument(0, named("org.springframework.context.ApplicationContext")))
61+
.and(takesArguments(1)),
62+
AppSecDispatcherServletInstrumentation.class.getName() + "$AppSecHandlerMappingAdvice");
63+
}
64+
65+
@Override
66+
public boolean isEnabled() {
67+
return super.isEnabled() && Config.get().isApiSecurityEndpointCollectionEnabled();
68+
}
69+
70+
public static class AppSecHandlerMappingAdvice {
71+
72+
@Advice.OnMethodExit(suppress = Throwable.class)
73+
public static void afterRefresh(@Advice.Argument(0) final ApplicationContext springCtx) {
74+
final RequestMappingHandlerMapping handler =
75+
springCtx.getBean(RequestMappingHandlerMapping.class);
76+
if (handler == null) {
77+
return;
78+
}
79+
final Map<RequestMappingInfo, HandlerMethod> mappings = handler.getHandlerMethods();
80+
if (mappings == null || mappings.isEmpty()) {
81+
return;
82+
}
83+
EndpointCollector.get().supplier(new RequestMappingInfoIterator(mappings));
84+
}
85+
}
86+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package datadog.trace.instrumentation.springweb;
2+
3+
import datadog.trace.api.telemetry.Endpoint;
4+
import datadog.trace.api.telemetry.Endpoint.Method;
5+
import java.util.ArrayList;
6+
import java.util.HashMap;
7+
import java.util.Iterator;
8+
import java.util.LinkedList;
9+
import java.util.List;
10+
import java.util.Map;
11+
import java.util.NoSuchElementException;
12+
import java.util.Queue;
13+
import java.util.Set;
14+
import org.springframework.web.method.HandlerMethod;
15+
import org.springframework.web.servlet.mvc.condition.MediaTypeExpression;
16+
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
17+
18+
public class RequestMappingInfoIterator implements Iterator<Endpoint> {
19+
20+
private final Map<RequestMappingInfo, HandlerMethod> mappings;
21+
private final Queue<Endpoint> queue = new LinkedList<>();
22+
private Iterator<Map.Entry<RequestMappingInfo, HandlerMethod>> iterator;
23+
private boolean first = true;
24+
25+
public RequestMappingInfoIterator(final Map<RequestMappingInfo, HandlerMethod> mappings) {
26+
this.mappings = mappings;
27+
}
28+
29+
private Iterator<Map.Entry<RequestMappingInfo, HandlerMethod>> iterator() {
30+
if (iterator == null) {
31+
iterator = mappings.entrySet().iterator();
32+
}
33+
return iterator;
34+
}
35+
36+
@Override
37+
public boolean hasNext() {
38+
return !queue.isEmpty() || iterator().hasNext();
39+
}
40+
41+
@Override
42+
public Endpoint next() {
43+
if (queue.isEmpty()) {
44+
fetchNext();
45+
}
46+
final Endpoint endpoint = queue.poll();
47+
if (endpoint == null) {
48+
throw new NoSuchElementException();
49+
}
50+
return endpoint;
51+
}
52+
53+
private void fetchNext() {
54+
final Iterator<Map.Entry<RequestMappingInfo, HandlerMethod>> delegate = iterator();
55+
if (!delegate.hasNext()) {
56+
return;
57+
}
58+
final Map.Entry<RequestMappingInfo, HandlerMethod> nextEntry = delegate.next();
59+
final RequestMappingInfo nextInfo = nextEntry.getKey();
60+
final HandlerMethod nextHandler = nextEntry.getValue();
61+
final List<String> requestBody =
62+
parseMediaTypes(nextInfo.getConsumesCondition().getExpressions());
63+
final List<String> responseBody =
64+
parseMediaTypes(nextInfo.getProducesCondition().getExpressions());
65+
for (final String path : nextInfo.getPatternsCondition().getPatterns()) {
66+
final List<String> methods = Method.parseMethods(nextInfo.getMethodsCondition().getMethods());
67+
for (final String method : methods) {
68+
Endpoint endpoint =
69+
new Endpoint()
70+
.type(Endpoint.Type.REST)
71+
.operation(Endpoint.Operation.HTTP_REQUEST)
72+
.path(path)
73+
.method(method)
74+
.requestBodyType(requestBody)
75+
.responseBodyType(responseBody);
76+
if (nextHandler != null) {
77+
final Map<String, String> metadata = new HashMap<>();
78+
metadata.put("handler", nextHandler.toString());
79+
endpoint.metadata(metadata);
80+
}
81+
if (first) {
82+
endpoint.first(true);
83+
first = false;
84+
}
85+
queue.add(endpoint);
86+
}
87+
}
88+
}
89+
90+
private List<String> parseMediaTypes(final Set<MediaTypeExpression> expressions) {
91+
if (expressions == null || expressions.isEmpty()) {
92+
return null;
93+
}
94+
final List<String> result = new ArrayList<>(expressions.size());
95+
for (final MediaTypeExpression expression : expressions) {
96+
result.add(expression.toString());
97+
}
98+
return result;
99+
}
100+
}

dd-java-agent/instrumentation/spring-webmvc-3.1/src/test/groovy/test/boot/SpringBootBasedTest.groovy

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import datadog.trace.api.iast.IastContext
99
import datadog.trace.api.iast.InstrumentationBridge
1010
import datadog.trace.api.iast.SourceTypes
1111
import datadog.trace.api.iast.propagation.PropagationModule
12+
import datadog.trace.api.telemetry.Endpoint
1213
import datadog.trace.bootstrap.instrumentation.api.Tags
1314
import datadog.trace.core.DDSpan
1415
import datadog.trace.instrumentation.springweb.SpringWebHttpServerDecorator
@@ -19,6 +20,7 @@ import okhttp3.Response
1920
import org.springframework.boot.SpringApplication
2021
import org.springframework.boot.context.embedded.EmbeddedWebApplicationContext
2122
import org.springframework.context.ConfigurableApplicationContext
23+
import org.springframework.http.MediaType
2224
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter
2325
import org.springframework.web.servlet.view.RedirectView
2426
import test.SetupSpecHelper
@@ -129,6 +131,22 @@ class SpringBootBasedTest extends HttpServerTest<ConfigurableApplicationContext>
129131
true
130132
}
131133

134+
@Override
135+
boolean testEndpointDiscovery() {
136+
true
137+
}
138+
139+
@Override
140+
void assertEndpointDiscovery(final List<?> endpoints) {
141+
final discovered = endpoints.collectEntries { [(it.method): it] } as Map<String, Endpoint>
142+
assert discovered.keySet().containsAll([Endpoint.Method.POST, Endpoint.Method.PATCH, Endpoint.Method.PUT])
143+
discovered.values().each {
144+
assert it.requestBodyType.containsAll([MediaType.APPLICATION_JSON_VALUE])
145+
assert it.responseBodyType.containsAll([MediaType.TEXT_PLAIN_VALUE])
146+
assert it.metadata['handler'] == 'public org.springframework.http.ResponseEntity test.boot.TestController.discovery()'
147+
}
148+
}
149+
132150
@Override
133151
Serializable expectedServerSpanRoute(ServerEndpoint endpoint) {
134152
switch (endpoint) {

dd-java-agent/instrumentation/spring-webmvc-3.1/src/test/groovy/test/boot/TestController.groovy

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import javax.servlet.http.HttpServletRequest
2222

2323
import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_JSON
2424
import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_URLENCODED
25+
import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.ENDPOINT_DISCOVERY
2526
import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.ERROR
2627
import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.EXCEPTION
2728
import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.FORWARDED
@@ -158,6 +159,16 @@ class TestController {
158159
}
159160
}
160161

162+
@RequestMapping(value = "/discovery",
163+
method = [RequestMethod.POST, RequestMethod.PATCH, RequestMethod.PUT],
164+
consumes = MediaType.APPLICATION_JSON_VALUE,
165+
produces = MediaType.TEXT_PLAIN_VALUE)
166+
ResponseEntity discovery() {
167+
HttpServerTest.controller(ENDPOINT_DISCOVERY) {
168+
new ResponseEntity(ENDPOINT_DISCOVERY.body, HttpStatus.valueOf(ENDPOINT_DISCOVERY.status))
169+
}
170+
}
171+
161172
@ExceptionHandler
162173
ResponseEntity handleException(Throwable throwable) {
163174
new ResponseEntity(throwable.message, HttpStatus.INTERNAL_SERVER_ERROR)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package datadog.trace.instrumentation.springweb;
2+
3+
import static datadog.trace.agent.tooling.bytebuddy.matcher.ClassLoaderMatchers.hasClassNamed;
4+
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
5+
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
6+
import static net.bytebuddy.matcher.ElementMatchers.isProtected;
7+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
8+
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
9+
10+
import com.google.auto.service.AutoService;
11+
import datadog.trace.agent.tooling.Instrumenter;
12+
import datadog.trace.agent.tooling.InstrumenterModule;
13+
import datadog.trace.api.Config;
14+
import datadog.trace.api.telemetry.EndpointCollector;
15+
import java.util.Map;
16+
import net.bytebuddy.asm.Advice;
17+
import net.bytebuddy.matcher.ElementMatcher;
18+
import org.springframework.context.ApplicationContext;
19+
import org.springframework.web.method.HandlerMethod;
20+
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
21+
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
22+
23+
@AutoService(InstrumenterModule.class)
24+
public class AppSecDispatcherServletWithPathPatternsInstrumentation
25+
extends InstrumenterModule.AppSec
26+
implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {
27+
28+
public AppSecDispatcherServletWithPathPatternsInstrumentation() {
29+
super("spring-web");
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[] {packageName + ".RequestMappingInfoWithPathPatternsIterator"};
40+
}
41+
42+
@Override
43+
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
44+
return hasClassNamed(
45+
"org.springframework.web.servlet.mvc.condition.PathPatternsRequestCondition");
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+
AppSecDispatcherServletWithPathPatternsInstrumentation.class.getName()
57+
+ "$AppSecHandlerMappingAdvice");
58+
}
59+
60+
@Override
61+
public boolean isEnabled() {
62+
return super.isEnabled() && Config.get().isApiSecurityEndpointCollectionEnabled();
63+
}
64+
65+
public static class AppSecHandlerMappingAdvice {
66+
67+
@Advice.OnMethodExit(suppress = Throwable.class)
68+
public static void afterRefresh(@Advice.Argument(0) final ApplicationContext springCtx) {
69+
final RequestMappingHandlerMapping handler =
70+
springCtx.getBean(RequestMappingHandlerMapping.class);
71+
if (handler == null) {
72+
return;
73+
}
74+
final Map<RequestMappingInfo, HandlerMethod> mappings = handler.getHandlerMethods();
75+
if (mappings == null || mappings.isEmpty()) {
76+
return;
77+
}
78+
EndpointCollector.get().supplier(new RequestMappingInfoWithPathPatternsIterator(mappings));
79+
}
80+
}
81+
}

0 commit comments

Comments
 (0)