Add instrumentation for spring error handling
Also add additional tests for spring boot
MODIFY
dd-java-agent/instrumentation/servlet-2/src/test/groovy/JettyServletTest.groovy
dd-java-agent/instrumentation/servlet-2/src/test/groovy/JettyServletTest.groovy
@@ -95,7 +95,9 @@ class JettyServletTest extends AgentTestRunner {
trace.size() == 1
def span = trace[0]
+ span.context().serviceName == "unnamed-java-app"
span.context().operationName == "servlet.request"
+ span.context().resourceName == "servlet.request"
span.context().spanType == DDSpanTypes.WEB_SERVLET
!span.context().getErrorFlag()
span.context().parentId != 0 // parent should be the okhttp call.
@@ -130,6 +132,7 @@ class JettyServletTest extends AgentTestRunner {
def span = trace[0]
span.context().operationName == "servlet.request"
+ span.context().resourceName == "servlet.request"
span.context().spanType == DDSpanTypes.WEB_SERVLET
span.context().getErrorFlag()
span.context().parentId != 0 // parent should be the okhttp call.

MODIFY
dd-java-agent/instrumentation/servlet-3/src/test/groovy/JettyServletTest.groovy
dd-java-agent/instrumentation/servlet-3/src/test/groovy/JettyServletTest.groovy
@@ -96,7 +96,9 @@ class JettyServletTest extends AgentTestRunner {
trace.size() == 1
def span = trace[0]
+ span.context().serviceName == "unnamed-java-app"
span.context().operationName == "servlet.request"
+ span.context().resourceName == "servlet.request"
span.context().spanType == DDSpanTypes.WEB_SERVLET
!span.context().getErrorFlag()
span.context().parentId != 0 // parent should be the okhttp call.
@@ -131,7 +133,9 @@ class JettyServletTest extends AgentTestRunner {
trace.size() == 1
def span = trace[0]
+ span.context().serviceName == "unnamed-java-app"
span.context().operationName == "servlet.request"
+ span.context().resourceName == "servlet.request"
span.context().spanType == DDSpanTypes.WEB_SERVLET
span.context().getErrorFlag()
span.context().parentId != 0 // parent should be the okhttp call.

MODIFY
dd-java-agent/instrumentation/servlet-3/src/test/groovy/TomcatServletTest.groovy
dd-java-agent/instrumentation/servlet-3/src/test/groovy/TomcatServletTest.groovy
@@ -95,7 +95,9 @@ class TomcatServletTest extends AgentTestRunner {
trace.size() == 1
def span = trace[0]
+ span.context().serviceName == "unnamed-java-app"
span.context().operationName == "servlet.request"
+ span.context().resourceName == "servlet.request"
span.context().spanType == DDSpanTypes.WEB_SERVLET
!span.context().getErrorFlag()
span.context().parentId != 0 // parent should be the okhttp call.
@@ -130,7 +132,9 @@ class TomcatServletTest extends AgentTestRunner {
trace.size() == 1
def span = trace[0]
+ span.context().serviceName == "unnamed-java-app"
span.context().operationName == "servlet.request"
+ span.context().resourceName == "servlet.request"
span.context().spanType == DDSpanTypes.WEB_SERVLET
span.context().getErrorFlag()
span.context().parentId != 0 // parent should be the okhttp call.

MODIFY
dd-java-agent/instrumentation/spring-web/spring-web.gradle
dd-java-agent/instrumentation/spring-web/spring-web.gradle
@@ -25,4 +25,19 @@ dependencies {
compile deps.bytebuddy
compile deps.opentracing
compile deps.autoservice
+
+ testCompile project(':dd-java-agent:testing')
+
+ // Include servlet instrumentation for verifying the tomcat requests
+ testCompile project(':dd-java-agent:instrumentation:servlet-3')
+
+ testCompile group: 'javax.validation', name: 'validation-api', version: '1.1.0.Final'
+ testCompile group: 'org.hibernate', name: 'hibernate-validator', version: '5.4.2.Final'
+
+ testCompile group: 'org.spockframework', name: 'spock-spring', version: '1.1-groovy-2.4'
+
+ testCompile group: 'org.springframework', name: 'spring-web', version: '4.3.14.RELEASE'
+ testCompile group: 'org.springframework', name: 'spring-webmvc', version: '4.3.14.RELEASE'
+ testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '1.5.10.RELEASE'
+ testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-tomcat', version: '1.5.10.RELEASE'
}

MODIFY
dd-java-agent/instrumentation/spring-web/src/main/java/datadog/trace/instrumentation/springweb/SpringWebInstrumentation.java
dd-java-agent/instrumentation/spring-web/src/main/java/datadog/trace/instrumentation/springweb/SpringWebInstrumentation.java
package datadog.trace.instrumentation.springweb;
import static datadog.trace.agent.tooling.ClassLoaderMatcher.classLoaderHasClassWithField;
+import static io.opentracing.log.Fields.ERROR_OBJECT;
import static net.bytebuddy.matcher.ElementMatchers.hasSuperType;
import static net.bytebuddy.matcher.ElementMatchers.isInterface;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
+import static net.bytebuddy.matcher.ElementMatchers.isProtected;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
import static net.bytebuddy.matcher.ElementMatchers.named;
@@ -16,7 +18,10 @@ import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.api.DDSpanTypes;
import datadog.trace.api.DDTags;
import io.opentracing.Scope;
+import io.opentracing.Span;
+import io.opentracing.tag.Tags;
import io.opentracing.util.GlobalTracer;
+import java.util.Collections;
import javax.servlet.http.HttpServletRequest;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
@@ -45,11 +50,20 @@ public final class SpringWebInstrumentation extends Instrumenter.Configurable {
.and(isPublic())
.and(nameStartsWith("handle"))
.and(takesArgument(0, named("javax.servlet.http.HttpServletRequest"))),
- SpringWebAdvice.class.getName()))
+ SpringWebNamingAdvice.class.getName()))
+ .type(not(isInterface()).and(named("org.springframework.web.servlet.DispatcherServlet")))
+ .transform(
+ DDAdvice.create()
+ .advice(
+ isMethod()
+ .and(isProtected())
+ .and(nameStartsWith("processHandlerException"))
+ .and(takesArgument(3, Exception.class)),
+ SpringWebErrorHandlerAdvice.class.getName()))
.asDecorator();
}
- public static class SpringWebAdvice {
+ public static class SpringWebNamingAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void nameResource(@Advice.Argument(0) final HttpServletRequest request) {
@@ -66,4 +80,16 @@ public final class SpringWebInstrumentation extends Instrumenter.Configurable {
}
}
}
+
+ public static class SpringWebErrorHandlerAdvice {
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void nameResource(@Advice.Argument(3) final Exception exception) {
+ final Scope scope = GlobalTracer.get().scopeManager().active();
+ if (scope != null && exception != null) {
+ final Span span = scope.span();
+ Tags.ERROR.set(span, true);
+ span.log(Collections.singletonMap(ERROR_OBJECT, exception));
+ }
+ }
+ }
}

ADD
dd-java-agent/instrumentation/spring-web/src/test/groovy/SpringBootBasedTest.groovy
+package test
+
+import datadog.trace.agent.test.AgentTestRunner
+import datadog.trace.api.DDSpanTypes
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.context.embedded.LocalServerPort
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment
+import org.springframework.boot.test.web.client.TestRestTemplate
+import org.springframework.web.bind.MethodArgumentNotValidException
+import org.springframework.web.util.NestedServletException
+
+@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
+class SpringBootBasedTest extends AgentTestRunner {
+
+ @LocalServerPort
+ private int port
+
+ @Autowired
+ private TestRestTemplate restTemplate
+
+ def "valid response"() {
+ expect:
+ port != 0
+ restTemplate.getForObject("http://localhost:$port/", String) == "Hello World"
+
+ and:
+ TEST_WRITER.waitForTraces(1)
+ TEST_WRITER.size() == 1
+ }
+
+ def "generates spans"() {
+ expect:
+ restTemplate.getForObject("http://localhost:$port/param/asdf1234/", String) == "Hello asdf1234"
+ TEST_WRITER.waitForTraces(1)
+ TEST_WRITER.size() == 1
+
+ def trace = TEST_WRITER.firstTrace()
+ trace.size() == 1
+ def span = trace[0]
+
+ span.context().operationName == "servlet.request"
+ span.context().resourceName == "GET /param/{parameter}/"
+ span.context().spanType == DDSpanTypes.WEB_SERVLET
+ !span.context().getErrorFlag()
+ span.context().parentId == 0
+ span.context().tags["http.url"] == "http://localhost:$port/param/asdf1234/"
+ span.context().tags["http.method"] == "GET"
+ span.context().tags["span.kind"] == "server"
+ span.context().tags["span.type"] == "web"
+ span.context().tags["component"] == "java-web-servlet"
+ span.context().tags["http.status_code"] == 200
+ span.context().tags["thread.name"] != null
+ span.context().tags["thread.id"] != null
+ span.context().tags.size() == 8
+ }
+
+ def "generates 404 spans"() {
+ def response = restTemplate.getForObject("http://localhost:$port/invalid", Map)
+ expect:
+ response.get("status") == 404
+ response.get("error") == "Not Found"
+ TEST_WRITER.waitForTraces(2)
+ TEST_WRITER.size() == 2
+
+ and: // trace 0
+ def trace0 = TEST_WRITER.get(0)
+ trace0.size() == 1
+ def span0 = trace0[0]
+
+ span0.context().operationName == "servlet.request"
+ span0.context().resourceName == "404"
+ span0.context().spanType == DDSpanTypes.WEB_SERVLET
+ !span0.context().getErrorFlag()
+ span0.context().parentId == 0
+ span0.context().tags["http.url"] == "http://localhost:$port/invalid"
+ span0.context().tags["http.method"] == "GET"
+ span0.context().tags["span.kind"] == "server"
+ span0.context().tags["span.type"] == "web"
+ span0.context().tags["component"] == "java-web-servlet"
+ span0.context().tags["http.status_code"] == 404
+ span0.context().tags["thread.name"] != null
+ span0.context().tags["thread.id"] != null
+ span0.context().tags.size() == 8
+
+ and: // trace 1
+ def trace1 = TEST_WRITER.get(1)
+ trace1.size() == 1
+ def span1 = trace1[0]
+
+ span1.context().operationName == "servlet.request"
+ span1.context().resourceName == "404"
+ span1.context().spanType == DDSpanTypes.WEB_SERVLET
+ !span1.context().getErrorFlag()
+ span1.context().parentId == 0
+ span1.context().tags["http.url"] == "http://localhost:$port/error"
+ span1.context().tags["http.method"] == "GET"
+ span1.context().tags["span.kind"] == "server"
+ span1.context().tags["span.type"] == "web"
+ span1.context().tags["component"] == "java-web-servlet"
+ span1.context().tags["http.status_code"] == 404
+ span1.context().tags["thread.name"] != null
+ span1.context().tags["thread.id"] != null
+ span1.context().tags.size() == 8
+ }
+
+ def "generates error spans"() {
+ expect:
+ def response = restTemplate.getForObject("http://localhost:$port/error/qwerty/", Map)
+ response.get("status") == 500
+ response.get("error") == "Internal Server Error"
+ response.get("exception") == "java.lang.RuntimeException"
+ response.get("message") == "qwerty"
+ TEST_WRITER.waitForTraces(2)
+ TEST_WRITER.size() == 2
+
+ and: // trace 0
+ def trace0 = TEST_WRITER.get(0)
+ trace0.size() == 1
+ def span0 = trace0[0]
+
+ span0.context().operationName == "servlet.request"
+ span0.context().resourceName == "GET /error/{parameter}/"
+ span0.context().spanType == DDSpanTypes.WEB_SERVLET
+ span0.context().getErrorFlag()
+ span0.context().parentId == 0
+ span0.context().tags["http.url"] == "http://localhost:$port/error/qwerty/"
+ span0.context().tags["http.method"] == "GET"
+ span0.context().tags["span.kind"] == "server"
+ span0.context().tags["span.type"] == "web"
+ span0.context().tags["component"] == "java-web-servlet"
+ span0.context().tags["http.status_code"] == 500
+ span0.context().tags["thread.name"] != null
+ span0.context().tags["thread.id"] != null
+ span0.context().tags["error"] == true
+ span0.context().tags["error.msg"] == "Request processing failed; nested exception is java.lang.RuntimeException: qwerty"
+ span0.context().tags["error.type"] == NestedServletException.getName()
+ span0.context().tags["error.stack"] != null
+ span0.context().tags.size() == 12
+
+ and: // trace 1
+ def trace1 = TEST_WRITER.get(1)
+ trace1.size() == 1
+ def span1 = trace1[0]
+
+ span1.context().operationName == "servlet.request"
+ span1.context().resourceName == "GET /error"
+ span1.context().spanType == DDSpanTypes.WEB_SERVLET
+ !span1.context().getErrorFlag()
+ span1.context().parentId == 0
+ span1.context().tags["http.url"] == "http://localhost:$port/error"
+ span1.context().tags["http.method"] == "GET"
+ span1.context().tags["span.kind"] == "server"
+ span1.context().tags["span.type"] == "web"
+ span1.context().tags["component"] == "java-web-servlet"
+ span1.context().tags["http.status_code"] == 500
+ span1.context().tags["thread.name"] != null
+ span1.context().tags["thread.id"] != null
+ span1.context().tags.size() == 8
+ }
+
+ def "validated form"() {
+ expect:
+ restTemplate.postForObject("http://localhost:$port/validated", new TestForm("bob", 20), String) == "Hello bob Person(Name: bob, Age: 20)"
+ TEST_WRITER.waitForTraces(1)
+ TEST_WRITER.size() == 1
+
+ def trace = TEST_WRITER.firstTrace()
+ trace.size() == 1
+ def span = trace[0]
+
+ span.context().operationName == "servlet.request"
+ span.context().resourceName == "POST /validated"
+ span.context().spanType == DDSpanTypes.WEB_SERVLET
+ !span.context().getErrorFlag()
+ span.context().parentId == 0
+ span.context().tags["http.url"] == "http://localhost:$port/validated"
+ span.context().tags["http.method"] == "POST"
+ span.context().tags["span.kind"] == "server"
+ span.context().tags["span.type"] == "web"
+ span.context().tags["component"] == "java-web-servlet"
+ span.context().tags["http.status_code"] == 200
+ span.context().tags["thread.name"] != null
+ span.context().tags["thread.id"] != null
+ span.context().tags.size() == 8
+ }
+
+ def "invalid form"() {
+ expect:
+ def response = restTemplate.postForObject("http://localhost:$port/validated", new TestForm("bill", 5), Map, Map)
+ response.get("status") == 400
+ response.get("error") == "Bad Request"
+ response.get("exception") == "org.springframework.web.bind.MethodArgumentNotValidException"
+ response.get("message") == "Validation failed for object='testForm'. Error count: 1"
+ TEST_WRITER.waitForTraces(2)
+ TEST_WRITER.size() == 2
+
+ and: // trace 0
+ def trace0 = TEST_WRITER.get(0)
+ trace0.size() == 1
+ def span0 = trace0[0]
+
+ span0.context().operationName == "servlet.request"
+ span0.context().resourceName == "POST /validated"
+ span0.context().spanType == DDSpanTypes.WEB_SERVLET
+ span0.context().getErrorFlag()
+ span0.context().parentId == 0
+ span0.context().tags["http.url"] == "http://localhost:$port/validated"
+ span0.context().tags["http.method"] == "POST"
+ span0.context().tags["span.kind"] == "server"
+ span0.context().tags["span.type"] == "web"
+ span0.context().tags["component"] == "java-web-servlet"
+ span0.context().tags["http.status_code"] == 400
+ span0.context().tags["thread.name"] != null
+ span0.context().tags["thread.id"] != null
+ span0.context().tags["error"] == true
+ span0.context().tags["error.msg"].toString().startsWith("Validation failed")
+ span0.context().tags["error.type"] == MethodArgumentNotValidException.getName()
+ span0.context().tags["error.stack"] != null
+ span0.context().tags.size() == 12
+
+ and: // trace 1
+ def trace1 = TEST_WRITER.get(1)
+ trace1.size() == 1
+ def span1 = trace1[0]
+
+ span1.context().operationName == "servlet.request"
+ span1.context().resourceName == "POST /error"
+ span1.context().spanType == DDSpanTypes.WEB_SERVLET
+ !span1.context().getErrorFlag()
+ span1.context().parentId == 0
+ span1.context().tags["http.url"] == "http://localhost:$port/error"
+ span1.context().tags["http.method"] == "POST"
+ span1.context().tags["span.kind"] == "server"
+ span1.context().tags["span.type"] == "web"
+ span1.context().tags["component"] == "java-web-servlet"
+ span1.context().tags["http.status_code"] == 400
+ span1.context().tags["thread.name"] != null
+ span1.context().tags["thread.id"] != null
+ span1.context().tags.size() == 8
+ }
+}

ADD
dd-java-agent/instrumentation/spring-web/src/test/java/test/Application.java
+package test;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+
+ public static void main(final String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+}

ADD
dd-java-agent/instrumentation/spring-web/src/test/java/test/TestController.java
+package test;
+
+import javax.validation.Valid;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+@Controller
+public class TestController {
+
+ @GetMapping("/")
+ public @ResponseBody String greeting() {
+ return "Hello World";
+ }
+
+ @GetMapping("/param/{parameter}/")
+ public @ResponseBody String withParam(@PathVariable("parameter") final String param) {
+ return "Hello " + param;
+ }
+
+ @PostMapping("/validated")
+ public @ResponseBody String withValidation(@Valid @RequestBody final TestForm form) {
+ return "Hello " + form.getName() + " " + form;
+ }
+
+ @GetMapping("/error/{parameter}/")
+ public @ResponseBody String withError(@PathVariable("parameter") final String param) {
+ throw new RuntimeException(param);
+ }
+}

ADD
dd-java-agent/instrumentation/spring-web/src/test/java/test/TestForm.java
+package test;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+
+public class TestForm {
+
+ @NotNull
+ @Size(min = 2, max = 30)
+ private String name;
+
+ @NotNull
+ @Min(18)
+ private Integer age;
+
+ public TestForm() {}
+
+ public TestForm(final String name, final Integer age) {
+ this.name = name;
+ this.age = age;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ public Integer getAge() {
+ return age;
+ }
+
+ public void setAge(final Integer age) {
+ this.age = age;
+ }
+
+ public String toString() {
+ return "Person(Name: " + this.name + ", Age: " + this.age + ")";
+ }
+}

MODIFY
gradle/dependencies.gradle
gradle/dependencies.gradle
@@ -9,7 +9,7 @@ ext {
guava : "20.0",
jackson : "2.6.3",
- spock : "1.0-groovy-$spockGroovyVer",
+ spock : "1.1-groovy-$spockGroovyVer",
groovy : groovyVer,
junit : "4.12",
logback : "1.2.3",

Add instrumentation for spring error handling

Also add additional tests for spring boot