Skip to content

Commit 3ec70ac

Browse files
committed
Add instrumentation for spring error handling
Also add additional tests for spring boot
1 parent 1905166 commit 3ec70ac

10 files changed

Lines changed: 385 additions & 3 deletions

File tree

dd-java-agent/instrumentation/servlet-2/src/test/groovy/JettyServletTest.groovy

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,9 @@ class JettyServletTest extends AgentTestRunner {
9595
trace.size() == 1
9696
def span = trace[0]
9797

98+
span.context().serviceName == "unnamed-java-app"
9899
span.context().operationName == "servlet.request"
100+
span.context().resourceName == "servlet.request"
99101
span.context().spanType == DDSpanTypes.WEB_SERVLET
100102
!span.context().getErrorFlag()
101103
span.context().parentId != 0 // parent should be the okhttp call.
@@ -130,6 +132,7 @@ class JettyServletTest extends AgentTestRunner {
130132
def span = trace[0]
131133

132134
span.context().operationName == "servlet.request"
135+
span.context().resourceName == "servlet.request"
133136
span.context().spanType == DDSpanTypes.WEB_SERVLET
134137
span.context().getErrorFlag()
135138
span.context().parentId != 0 // parent should be the okhttp call.

dd-java-agent/instrumentation/servlet-3/src/test/groovy/JettyServletTest.groovy

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,9 @@ class JettyServletTest extends AgentTestRunner {
9696
trace.size() == 1
9797
def span = trace[0]
9898

99+
span.context().serviceName == "unnamed-java-app"
99100
span.context().operationName == "servlet.request"
101+
span.context().resourceName == "servlet.request"
100102
span.context().spanType == DDSpanTypes.WEB_SERVLET
101103
!span.context().getErrorFlag()
102104
span.context().parentId != 0 // parent should be the okhttp call.
@@ -131,7 +133,9 @@ class JettyServletTest extends AgentTestRunner {
131133
trace.size() == 1
132134
def span = trace[0]
133135

136+
span.context().serviceName == "unnamed-java-app"
134137
span.context().operationName == "servlet.request"
138+
span.context().resourceName == "servlet.request"
135139
span.context().spanType == DDSpanTypes.WEB_SERVLET
136140
span.context().getErrorFlag()
137141
span.context().parentId != 0 // parent should be the okhttp call.

dd-java-agent/instrumentation/servlet-3/src/test/groovy/TomcatServletTest.groovy

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,9 @@ class TomcatServletTest extends AgentTestRunner {
9595
trace.size() == 1
9696
def span = trace[0]
9797

98+
span.context().serviceName == "unnamed-java-app"
9899
span.context().operationName == "servlet.request"
100+
span.context().resourceName == "servlet.request"
99101
span.context().spanType == DDSpanTypes.WEB_SERVLET
100102
!span.context().getErrorFlag()
101103
span.context().parentId != 0 // parent should be the okhttp call.
@@ -130,7 +132,9 @@ class TomcatServletTest extends AgentTestRunner {
130132
trace.size() == 1
131133
def span = trace[0]
132134

135+
span.context().serviceName == "unnamed-java-app"
133136
span.context().operationName == "servlet.request"
137+
span.context().resourceName == "servlet.request"
134138
span.context().spanType == DDSpanTypes.WEB_SERVLET
135139
span.context().getErrorFlag()
136140
span.context().parentId != 0 // parent should be the okhttp call.

dd-java-agent/instrumentation/spring-web/spring-web.gradle

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,19 @@ dependencies {
2525
compile deps.bytebuddy
2626
compile deps.opentracing
2727
compile deps.autoservice
28+
29+
testCompile project(':dd-java-agent:testing')
30+
31+
// Include servlet instrumentation for verifying the tomcat requests
32+
testCompile project(':dd-java-agent:instrumentation:servlet-3')
33+
34+
testCompile group: 'javax.validation', name: 'validation-api', version: '1.1.0.Final'
35+
testCompile group: 'org.hibernate', name: 'hibernate-validator', version: '5.4.2.Final'
36+
37+
testCompile group: 'org.spockframework', name: 'spock-spring', version: '1.1-groovy-2.4'
38+
39+
testCompile group: 'org.springframework', name: 'spring-web', version: '4.3.14.RELEASE'
40+
testCompile group: 'org.springframework', name: 'spring-webmvc', version: '4.3.14.RELEASE'
41+
testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '1.5.10.RELEASE'
42+
testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-tomcat', version: '1.5.10.RELEASE'
2843
}

dd-java-agent/instrumentation/spring-web/src/main/java/datadog/trace/instrumentation/springweb/SpringWebInstrumentation.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package datadog.trace.instrumentation.springweb;
22

33
import static datadog.trace.agent.tooling.ClassLoaderMatcher.classLoaderHasClassWithField;
4+
import static io.opentracing.log.Fields.ERROR_OBJECT;
45
import static net.bytebuddy.matcher.ElementMatchers.hasSuperType;
56
import static net.bytebuddy.matcher.ElementMatchers.isInterface;
67
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
8+
import static net.bytebuddy.matcher.ElementMatchers.isProtected;
79
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
810
import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
911
import static net.bytebuddy.matcher.ElementMatchers.named;
@@ -16,7 +18,10 @@
1618
import datadog.trace.api.DDSpanTypes;
1719
import datadog.trace.api.DDTags;
1820
import io.opentracing.Scope;
21+
import io.opentracing.Span;
22+
import io.opentracing.tag.Tags;
1923
import io.opentracing.util.GlobalTracer;
24+
import java.util.Collections;
2025
import javax.servlet.http.HttpServletRequest;
2126
import net.bytebuddy.agent.builder.AgentBuilder;
2227
import net.bytebuddy.asm.Advice;
@@ -45,11 +50,20 @@ public AgentBuilder apply(final AgentBuilder agentBuilder) {
4550
.and(isPublic())
4651
.and(nameStartsWith("handle"))
4752
.and(takesArgument(0, named("javax.servlet.http.HttpServletRequest"))),
48-
SpringWebAdvice.class.getName()))
53+
SpringWebNamingAdvice.class.getName()))
54+
.type(not(isInterface()).and(named("org.springframework.web.servlet.DispatcherServlet")))
55+
.transform(
56+
DDAdvice.create()
57+
.advice(
58+
isMethod()
59+
.and(isProtected())
60+
.and(nameStartsWith("processHandlerException"))
61+
.and(takesArgument(3, Exception.class)),
62+
SpringWebErrorHandlerAdvice.class.getName()))
4963
.asDecorator();
5064
}
5165

52-
public static class SpringWebAdvice {
66+
public static class SpringWebNamingAdvice {
5367

5468
@Advice.OnMethodEnter(suppress = Throwable.class)
5569
public static void nameResource(@Advice.Argument(0) final HttpServletRequest request) {
@@ -66,4 +80,16 @@ public static void nameResource(@Advice.Argument(0) final HttpServletRequest req
6680
}
6781
}
6882
}
83+
84+
public static class SpringWebErrorHandlerAdvice {
85+
@Advice.OnMethodEnter(suppress = Throwable.class)
86+
public static void nameResource(@Advice.Argument(3) final Exception exception) {
87+
final Scope scope = GlobalTracer.get().scopeManager().active();
88+
if (scope != null && exception != null) {
89+
final Span span = scope.span();
90+
Tags.ERROR.set(span, true);
91+
span.log(Collections.singletonMap(ERROR_OBJECT, exception));
92+
}
93+
}
94+
}
6995
}
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
package test
2+
3+
import datadog.trace.agent.test.AgentTestRunner
4+
import datadog.trace.api.DDSpanTypes
5+
import org.springframework.beans.factory.annotation.Autowired
6+
import org.springframework.boot.context.embedded.LocalServerPort
7+
import org.springframework.boot.test.context.SpringBootTest
8+
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment
9+
import org.springframework.boot.test.web.client.TestRestTemplate
10+
import org.springframework.web.bind.MethodArgumentNotValidException
11+
import org.springframework.web.util.NestedServletException
12+
13+
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
14+
class SpringBootBasedTest extends AgentTestRunner {
15+
16+
@LocalServerPort
17+
private int port
18+
19+
@Autowired
20+
private TestRestTemplate restTemplate
21+
22+
def "valid response"() {
23+
expect:
24+
port != 0
25+
restTemplate.getForObject("http://localhost:$port/", String) == "Hello World"
26+
27+
and:
28+
TEST_WRITER.waitForTraces(1)
29+
TEST_WRITER.size() == 1
30+
}
31+
32+
def "generates spans"() {
33+
expect:
34+
restTemplate.getForObject("http://localhost:$port/param/asdf1234/", String) == "Hello asdf1234"
35+
TEST_WRITER.waitForTraces(1)
36+
TEST_WRITER.size() == 1
37+
38+
def trace = TEST_WRITER.firstTrace()
39+
trace.size() == 1
40+
def span = trace[0]
41+
42+
span.context().operationName == "servlet.request"
43+
span.context().resourceName == "GET /param/{parameter}/"
44+
span.context().spanType == DDSpanTypes.WEB_SERVLET
45+
!span.context().getErrorFlag()
46+
span.context().parentId == 0
47+
span.context().tags["http.url"] == "http://localhost:$port/param/asdf1234/"
48+
span.context().tags["http.method"] == "GET"
49+
span.context().tags["span.kind"] == "server"
50+
span.context().tags["span.type"] == "web"
51+
span.context().tags["component"] == "java-web-servlet"
52+
span.context().tags["http.status_code"] == 200
53+
span.context().tags["thread.name"] != null
54+
span.context().tags["thread.id"] != null
55+
span.context().tags.size() == 8
56+
}
57+
58+
def "generates 404 spans"() {
59+
def response = restTemplate.getForObject("http://localhost:$port/invalid", Map)
60+
expect:
61+
response.get("status") == 404
62+
response.get("error") == "Not Found"
63+
TEST_WRITER.waitForTraces(2)
64+
TEST_WRITER.size() == 2
65+
66+
and: // trace 0
67+
def trace0 = TEST_WRITER.get(0)
68+
trace0.size() == 1
69+
def span0 = trace0[0]
70+
71+
span0.context().operationName == "servlet.request"
72+
span0.context().resourceName == "404"
73+
span0.context().spanType == DDSpanTypes.WEB_SERVLET
74+
!span0.context().getErrorFlag()
75+
span0.context().parentId == 0
76+
span0.context().tags["http.url"] == "http://localhost:$port/invalid"
77+
span0.context().tags["http.method"] == "GET"
78+
span0.context().tags["span.kind"] == "server"
79+
span0.context().tags["span.type"] == "web"
80+
span0.context().tags["component"] == "java-web-servlet"
81+
span0.context().tags["http.status_code"] == 404
82+
span0.context().tags["thread.name"] != null
83+
span0.context().tags["thread.id"] != null
84+
span0.context().tags.size() == 8
85+
86+
and: // trace 1
87+
def trace1 = TEST_WRITER.get(1)
88+
trace1.size() == 1
89+
def span1 = trace1[0]
90+
91+
span1.context().operationName == "servlet.request"
92+
span1.context().resourceName == "404"
93+
span1.context().spanType == DDSpanTypes.WEB_SERVLET
94+
!span1.context().getErrorFlag()
95+
span1.context().parentId == 0
96+
span1.context().tags["http.url"] == "http://localhost:$port/error"
97+
span1.context().tags["http.method"] == "GET"
98+
span1.context().tags["span.kind"] == "server"
99+
span1.context().tags["span.type"] == "web"
100+
span1.context().tags["component"] == "java-web-servlet"
101+
span1.context().tags["http.status_code"] == 404
102+
span1.context().tags["thread.name"] != null
103+
span1.context().tags["thread.id"] != null
104+
span1.context().tags.size() == 8
105+
}
106+
107+
def "generates error spans"() {
108+
expect:
109+
def response = restTemplate.getForObject("http://localhost:$port/error/qwerty/", Map)
110+
response.get("status") == 500
111+
response.get("error") == "Internal Server Error"
112+
response.get("exception") == "java.lang.RuntimeException"
113+
response.get("message") == "qwerty"
114+
TEST_WRITER.waitForTraces(2)
115+
TEST_WRITER.size() == 2
116+
117+
and: // trace 0
118+
def trace0 = TEST_WRITER.get(0)
119+
trace0.size() == 1
120+
def span0 = trace0[0]
121+
122+
span0.context().operationName == "servlet.request"
123+
span0.context().resourceName == "GET /error/{parameter}/"
124+
span0.context().spanType == DDSpanTypes.WEB_SERVLET
125+
span0.context().getErrorFlag()
126+
span0.context().parentId == 0
127+
span0.context().tags["http.url"] == "http://localhost:$port/error/qwerty/"
128+
span0.context().tags["http.method"] == "GET"
129+
span0.context().tags["span.kind"] == "server"
130+
span0.context().tags["span.type"] == "web"
131+
span0.context().tags["component"] == "java-web-servlet"
132+
span0.context().tags["http.status_code"] == 500
133+
span0.context().tags["thread.name"] != null
134+
span0.context().tags["thread.id"] != null
135+
span0.context().tags["error"] == true
136+
span0.context().tags["error.msg"] == "Request processing failed; nested exception is java.lang.RuntimeException: qwerty"
137+
span0.context().tags["error.type"] == NestedServletException.getName()
138+
span0.context().tags["error.stack"] != null
139+
span0.context().tags.size() == 12
140+
141+
and: // trace 1
142+
def trace1 = TEST_WRITER.get(1)
143+
trace1.size() == 1
144+
def span1 = trace1[0]
145+
146+
span1.context().operationName == "servlet.request"
147+
span1.context().resourceName == "GET /error"
148+
span1.context().spanType == DDSpanTypes.WEB_SERVLET
149+
!span1.context().getErrorFlag()
150+
span1.context().parentId == 0
151+
span1.context().tags["http.url"] == "http://localhost:$port/error"
152+
span1.context().tags["http.method"] == "GET"
153+
span1.context().tags["span.kind"] == "server"
154+
span1.context().tags["span.type"] == "web"
155+
span1.context().tags["component"] == "java-web-servlet"
156+
span1.context().tags["http.status_code"] == 500
157+
span1.context().tags["thread.name"] != null
158+
span1.context().tags["thread.id"] != null
159+
span1.context().tags.size() == 8
160+
}
161+
162+
def "validated form"() {
163+
expect:
164+
restTemplate.postForObject("http://localhost:$port/validated", new TestForm("bob", 20), String) == "Hello bob Person(Name: bob, Age: 20)"
165+
TEST_WRITER.waitForTraces(1)
166+
TEST_WRITER.size() == 1
167+
168+
def trace = TEST_WRITER.firstTrace()
169+
trace.size() == 1
170+
def span = trace[0]
171+
172+
span.context().operationName == "servlet.request"
173+
span.context().resourceName == "POST /validated"
174+
span.context().spanType == DDSpanTypes.WEB_SERVLET
175+
!span.context().getErrorFlag()
176+
span.context().parentId == 0
177+
span.context().tags["http.url"] == "http://localhost:$port/validated"
178+
span.context().tags["http.method"] == "POST"
179+
span.context().tags["span.kind"] == "server"
180+
span.context().tags["span.type"] == "web"
181+
span.context().tags["component"] == "java-web-servlet"
182+
span.context().tags["http.status_code"] == 200
183+
span.context().tags["thread.name"] != null
184+
span.context().tags["thread.id"] != null
185+
span.context().tags.size() == 8
186+
}
187+
188+
def "invalid form"() {
189+
expect:
190+
def response = restTemplate.postForObject("http://localhost:$port/validated", new TestForm("bill", 5), Map, Map)
191+
response.get("status") == 400
192+
response.get("error") == "Bad Request"
193+
response.get("exception") == "org.springframework.web.bind.MethodArgumentNotValidException"
194+
response.get("message") == "Validation failed for object='testForm'. Error count: 1"
195+
TEST_WRITER.waitForTraces(2)
196+
TEST_WRITER.size() == 2
197+
198+
and: // trace 0
199+
def trace0 = TEST_WRITER.get(0)
200+
trace0.size() == 1
201+
def span0 = trace0[0]
202+
203+
span0.context().operationName == "servlet.request"
204+
span0.context().resourceName == "POST /validated"
205+
span0.context().spanType == DDSpanTypes.WEB_SERVLET
206+
span0.context().getErrorFlag()
207+
span0.context().parentId == 0
208+
span0.context().tags["http.url"] == "http://localhost:$port/validated"
209+
span0.context().tags["http.method"] == "POST"
210+
span0.context().tags["span.kind"] == "server"
211+
span0.context().tags["span.type"] == "web"
212+
span0.context().tags["component"] == "java-web-servlet"
213+
span0.context().tags["http.status_code"] == 400
214+
span0.context().tags["thread.name"] != null
215+
span0.context().tags["thread.id"] != null
216+
span0.context().tags["error"] == true
217+
span0.context().tags["error.msg"].toString().startsWith("Validation failed")
218+
span0.context().tags["error.type"] == MethodArgumentNotValidException.getName()
219+
span0.context().tags["error.stack"] != null
220+
span0.context().tags.size() == 12
221+
222+
and: // trace 1
223+
def trace1 = TEST_WRITER.get(1)
224+
trace1.size() == 1
225+
def span1 = trace1[0]
226+
227+
span1.context().operationName == "servlet.request"
228+
span1.context().resourceName == "POST /error"
229+
span1.context().spanType == DDSpanTypes.WEB_SERVLET
230+
!span1.context().getErrorFlag()
231+
span1.context().parentId == 0
232+
span1.context().tags["http.url"] == "http://localhost:$port/error"
233+
span1.context().tags["http.method"] == "POST"
234+
span1.context().tags["span.kind"] == "server"
235+
span1.context().tags["span.type"] == "web"
236+
span1.context().tags["component"] == "java-web-servlet"
237+
span1.context().tags["http.status_code"] == 400
238+
span1.context().tags["thread.name"] != null
239+
span1.context().tags["thread.id"] != null
240+
span1.context().tags.size() == 8
241+
}
242+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package test;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
6+
@SpringBootApplication
7+
public class Application {
8+
9+
public static void main(final String[] args) {
10+
SpringApplication.run(Application.class, args);
11+
}
12+
}

0 commit comments

Comments
 (0)