Add new integration for Servlet Filters, HttpServlet, and RequestDispatcher

Disabled by default, and only creates a span if existing trace detected.

To enable all of them:
* System Property: `-Ddd.integration.servlet.enabled=true`
* Environment Variable: `DD_INTEGRATION_SERVLET_ENABLED=true`

(They have independent configs as well.  If needed, view the source below.)
This commit is contained in:
Tyler Benson 2019-10-25 10:40:37 -07:00
parent df387b486a
commit 425156115c
18 changed files with 1367 additions and 3 deletions

View File

@ -5,7 +5,7 @@ import java.io.Closeable;
public interface AgentScope extends Closeable {
AgentSpan span();
void setAsyncPropagation(boolean value);
AgentScope setAsyncPropagation(boolean value);
@Override
void close();

View File

@ -208,7 +208,9 @@ public class AgentTracer {
}
@Override
public void setAsyncPropagation(final boolean value) {}
public AgentScope setAsyncPropagation(final boolean value) {
return this;
}
@Override
public void close() {}

View File

@ -237,10 +237,11 @@ public final class OpenTracing32 implements TracerAPI {
}
@Override
public void setAsyncPropagation(final boolean value) {
public AgentScope setAsyncPropagation(final boolean value) {
if (scope instanceof TraceScope) {
((TraceScope) scope).setAsyncPropagation(value);
}
return this;
}
@Override

View File

@ -0,0 +1,14 @@
package datadog.trace.instrumentation.servlet;
import datadog.trace.instrumentation.api.AgentPropagation;
import javax.servlet.ServletRequest;
/** Inject into request attributes since the request headers can't be modified. */
public class ServletRequestSetter implements AgentPropagation.Setter<ServletRequest> {
public static final ServletRequestSetter SETTER = new ServletRequestSetter();
@Override
public void set(final ServletRequest carrier, final String key, final String value) {
carrier.setAttribute(key, value);
}
}

View File

@ -0,0 +1,22 @@
package datadog.trace.instrumentation.servlet.dispatcher;
import datadog.trace.agent.decorator.BaseDecorator;
public class RequestDispatcherDecorator extends BaseDecorator {
public static final RequestDispatcherDecorator DECORATE = new RequestDispatcherDecorator();
@Override
protected String[] instrumentationNames() {
return new String[] {"servlet", "servlet-dispatcher"};
}
@Override
protected String spanType() {
return null;
}
@Override
protected String component() {
return "java-web-servlet-dispatcher";
}
}

View File

@ -0,0 +1,108 @@
package datadog.trace.instrumentation.servlet.dispatcher;
import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType;
import static datadog.trace.instrumentation.api.AgentTracer.activateSpan;
import static datadog.trace.instrumentation.api.AgentTracer.activeSpan;
import static datadog.trace.instrumentation.api.AgentTracer.propagate;
import static datadog.trace.instrumentation.api.AgentTracer.startSpan;
import static datadog.trace.instrumentation.servlet.ServletRequestSetter.SETTER;
import static datadog.trace.instrumentation.servlet.dispatcher.RequestDispatcherDecorator.DECORATE;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.isInterface;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.not;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import com.google.auto.service.AutoService;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.api.DDTags;
import datadog.trace.bootstrap.InstrumentationContext;
import datadog.trace.instrumentation.api.AgentScope;
import datadog.trace.instrumentation.api.AgentSpan;
import java.util.Map;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletRequest;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
@AutoService(Instrumenter.class)
public final class RequestDispatcherInstrumentation extends Instrumenter.Default {
public RequestDispatcherInstrumentation() {
super("servlet", "servlet-dispatcher");
}
@Override
public boolean defaultEnabled() {
return false;
}
@Override
public String[] helperClassNames() {
return new String[] {
"datadog.trace.instrumentation.servlet.ServletRequestSetter",
"datadog.trace.agent.decorator.BaseDecorator",
packageName + ".RequestDispatcherDecorator",
};
}
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return not(isInterface()).and(safeHasSuperType(named("javax.servlet.RequestDispatcher")));
}
@Override
public Map<String, String> contextStore() {
return singletonMap("javax.servlet.RequestDispatcher", String.class.getName());
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
named("forward")
.or(named("include"))
.and(takesArgument(0, named("javax.servlet.ServletRequest")))
.and(takesArgument(1, named("javax.servlet.ServletResponse")))
.and(isPublic()),
RequestDispatcherAdvice.class.getName());
}
public static class RequestDispatcherAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static AgentScope start(
@Advice.Origin("#m") final String method,
@Advice.This final RequestDispatcher dispatcher,
@Advice.Argument(0) final ServletRequest request) {
if (activeSpan() == null) {
// Don't want to generate a new top-level span
return null;
}
final AgentSpan span = startSpan("servlet." + method);
DECORATE.afterStart(span);
final String target =
InstrumentationContext.get(RequestDispatcher.class, String.class).get(dispatcher);
span.setTag(DDTags.RESOURCE_NAME, target);
// In case we lose context, inject trace into to the request.
propagate().inject(span, request, SETTER);
return activateSpan(span, true).setAsyncPropagation(true);
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void stop(
@Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable throwable) {
if (scope == null) {
return;
}
DECORATE.onError(scope, throwable);
DECORATE.beforeFinish(scope);
scope.close();
}
}
}

View File

@ -0,0 +1,62 @@
package datadog.trace.instrumentation.servlet.dispatcher;
import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.isInterface;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.not;
import static net.bytebuddy.matcher.ElementMatchers.returns;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import com.google.auto.service.AutoService;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.bootstrap.InstrumentationContext;
import java.util.Map;
import javax.servlet.RequestDispatcher;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
@AutoService(Instrumenter.class)
public final class ServletContextInstrumentation extends Instrumenter.Default {
public ServletContextInstrumentation() {
super("servlet", "servlet-dispatcher");
}
@Override
public boolean defaultEnabled() {
return false;
}
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return not(isInterface()).and(safeHasSuperType(named("javax.servlet.ServletContext")));
}
@Override
public Map<String, String> contextStore() {
return singletonMap("javax.servlet.RequestDispatcher", String.class.getName());
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
returns(named("javax.servlet.RequestDispatcher"))
.and(takesArgument(0, String.class))
// javax.servlet.ServletContext.getRequestDispatcher
// javax.servlet.ServletContext.getNamedDispatcher
.and(isPublic()),
RequestDispatcherTargetAdvice.class.getName());
}
public static class RequestDispatcherTargetAdvice {
@Advice.OnMethodExit(suppress = Throwable.class)
public static void saveTarget(
@Advice.Argument(0) final String target,
@Advice.Return final RequestDispatcher dispatcher) {
InstrumentationContext.get(RequestDispatcher.class, String.class).put(dispatcher, target);
}
}
}

View File

@ -0,0 +1,22 @@
package datadog.trace.instrumentation.servlet.filter;
import datadog.trace.agent.decorator.BaseDecorator;
public class FilterDecorator extends BaseDecorator {
public static final FilterDecorator DECORATE = new FilterDecorator();
@Override
protected String[] instrumentationNames() {
return new String[] {"servlet", "servlet-filter"};
}
@Override
protected String spanType() {
return null;
}
@Override
protected String component() {
return "java-web-servlet-filter";
}
}

View File

@ -0,0 +1,89 @@
package datadog.trace.instrumentation.servlet.filter;
import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType;
import static datadog.trace.instrumentation.api.AgentTracer.activateSpan;
import static datadog.trace.instrumentation.api.AgentTracer.activeSpan;
import static datadog.trace.instrumentation.api.AgentTracer.startSpan;
import static datadog.trace.instrumentation.servlet.filter.FilterDecorator.DECORATE;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.isInterface;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.not;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import com.google.auto.service.AutoService;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.api.DDTags;
import datadog.trace.instrumentation.api.AgentScope;
import datadog.trace.instrumentation.api.AgentSpan;
import java.util.Map;
import javax.servlet.Filter;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
@AutoService(Instrumenter.class)
public final class FilterInstrumentation extends Instrumenter.Default {
public FilterInstrumentation() {
super("servlet", "servlet-filter");
}
@Override
public boolean defaultEnabled() {
return false;
}
@Override
public String[] helperClassNames() {
return new String[] {
"datadog.trace.agent.decorator.BaseDecorator", packageName + ".FilterDecorator",
};
}
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return not(isInterface()).and(safeHasSuperType(named("javax.servlet.Filter")));
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
named("doFilter")
.and(takesArgument(0, named("javax.servlet.ServletRequest")))
.and(takesArgument(1, named("javax.servlet.ServletResponse")))
.and(isPublic()),
FilterAdvice.class.getName());
}
public static class FilterAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static AgentScope start(@Advice.This final Filter filter) {
if (activeSpan() == null) {
// Don't want to generate a new top-level span
return null;
}
final AgentSpan span = startSpan("servlet.filter");
DECORATE.afterStart(span);
// Here we use "this" instead of "the method target" to distinguish abstract filter instances.
span.setTag(DDTags.RESOURCE_NAME, filter.getClass().getSimpleName() + ".doFilter");
return activateSpan(span, true).setAsyncPropagation(true);
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void stopSpan(
@Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable throwable) {
if (scope == null) {
return;
}
DECORATE.onError(scope, throwable);
DECORATE.beforeFinish(scope);
scope.close();
}
}
}

View File

@ -0,0 +1,22 @@
package datadog.trace.instrumentation.servlet.http;
import datadog.trace.agent.decorator.BaseDecorator;
public class HttpServletDecorator extends BaseDecorator {
public static final HttpServletDecorator DECORATE = new HttpServletDecorator();
@Override
protected String[] instrumentationNames() {
return new String[] {"servlet", "servlet-service"};
}
@Override
protected String spanType() {
return null;
}
@Override
protected String component() {
return "java-web-servlet-service";
}
}

View File

@ -0,0 +1,112 @@
package datadog.trace.instrumentation.servlet.http;
import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType;
import static datadog.trace.instrumentation.api.AgentTracer.activateSpan;
import static datadog.trace.instrumentation.api.AgentTracer.activeSpan;
import static datadog.trace.instrumentation.api.AgentTracer.startSpan;
import static datadog.trace.instrumentation.servlet.http.HttpServletDecorator.DECORATE;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.isInterface;
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;
import static net.bytebuddy.matcher.ElementMatchers.not;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import com.google.auto.service.AutoService;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.api.DDTags;
import datadog.trace.bootstrap.InstrumentationContext;
import datadog.trace.instrumentation.api.AgentScope;
import datadog.trace.instrumentation.api.AgentSpan;
import java.lang.reflect.Method;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
@AutoService(Instrumenter.class)
public final class HttpServletInstrumentation extends Instrumenter.Default {
public HttpServletInstrumentation() {
super("servlet", "servlet-service");
}
@Override
public boolean defaultEnabled() {
return false;
}
@Override
public String[] helperClassNames() {
return new String[] {
"datadog.trace.agent.decorator.BaseDecorator", packageName + ".HttpServletDecorator",
};
}
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return not(isInterface()).and(safeHasSuperType(named("javax.servlet.http.HttpServlet")));
}
/**
* Here we are instrumenting the protected method for HttpServlet. This should ensure that this
* advice is always called after Servlet3Instrumentation which is instrumenting the public method.
*/
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
named("service")
.or(nameStartsWith("do")) // doGet, doPost, etc
.and(takesArgument(0, named("javax.servlet.http.HttpServletRequest")))
.and(takesArgument(1, named("javax.servlet.http.HttpServletResponse")))
.and(isProtected().or(isPublic())),
HttpServletAdvice.class.getName());
}
@Override
public Map<String, String> contextStore() {
return singletonMap(
"javax.servlet.http.HttpServletResponse", "javax.servlet.http.HttpServletRequest");
}
public static class HttpServletAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static AgentScope start(
@Advice.Origin final Method method,
@Advice.Argument(0) final HttpServletRequest request,
@Advice.Argument(1) final HttpServletResponse response) {
// For use by HttpServletResponseInstrumentation:
InstrumentationContext.get(HttpServletResponse.class, HttpServletRequest.class)
.put(response, request);
if (activeSpan() == null) {
// Don't want to generate a new top-level span
return null;
}
final AgentSpan span = startSpan("servlet." + method.getName());
DECORATE.afterStart(span);
// Here we use the Method instead of "this.class.name" to distinguish calls to "super".
span.setTag(DDTags.RESOURCE_NAME, DECORATE.spanNameForMethod(method));
return activateSpan(span, true).setAsyncPropagation(true);
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void stopSpan(
@Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable throwable) {
if (scope == null) {
return;
}
DECORATE.onError(scope, throwable);
DECORATE.beforeFinish(scope);
scope.close();
}
}
}

View File

@ -0,0 +1,22 @@
package datadog.trace.instrumentation.servlet.http;
import datadog.trace.agent.decorator.BaseDecorator;
public class HttpServletResponseDecorator extends BaseDecorator {
public static final HttpServletResponseDecorator DECORATE = new HttpServletResponseDecorator();
@Override
protected String[] instrumentationNames() {
return new String[] {"servlet", "servlet-response"};
}
@Override
protected String spanType() {
return null;
}
@Override
protected String component() {
return "java-web-servlet-response";
}
}

View File

@ -0,0 +1,105 @@
package datadog.trace.instrumentation.servlet.http;
import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType;
import static datadog.trace.instrumentation.api.AgentTracer.activateSpan;
import static datadog.trace.instrumentation.api.AgentTracer.activeSpan;
import static datadog.trace.instrumentation.api.AgentTracer.propagate;
import static datadog.trace.instrumentation.api.AgentTracer.startSpan;
import static datadog.trace.instrumentation.servlet.ServletRequestSetter.SETTER;
import static datadog.trace.instrumentation.servlet.http.HttpServletResponseDecorator.DECORATE;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.isInterface;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.not;
import com.google.auto.service.AutoService;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.api.DDTags;
import datadog.trace.bootstrap.InstrumentationContext;
import datadog.trace.instrumentation.api.AgentScope;
import datadog.trace.instrumentation.api.AgentSpan;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
@AutoService(Instrumenter.class)
public final class HttpServletResponseInstrumentation extends Instrumenter.Default {
public HttpServletResponseInstrumentation() {
super("servlet", "servlet-response");
}
@Override
public boolean defaultEnabled() {
return false;
}
@Override
public String[] helperClassNames() {
return new String[] {
"datadog.trace.instrumentation.servlet.ServletRequestSetter",
"datadog.trace.agent.decorator.BaseDecorator",
packageName + ".HttpServletResponseDecorator",
};
}
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return not(isInterface())
.and(safeHasSuperType(named("javax.servlet.http.HttpServletResponse")));
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(named("sendError").or(named("sendRedirect")), SendAdvice.class.getName());
}
@Override
public Map<String, String> contextStore() {
return singletonMap(
"javax.servlet.http.HttpServletResponse", "javax.servlet.http.HttpServletRequest");
}
public static class SendAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static AgentScope start(
@Advice.Origin("#m") final String method, @Advice.This final HttpServletResponse resp) {
if (activeSpan() == null) {
// Don't want to generate a new top-level span
return null;
}
final HttpServletRequest req =
InstrumentationContext.get(HttpServletResponse.class, HttpServletRequest.class).get(resp);
if (req == null) {
// Missing the response->request linking... probably in a wrapped instance.
return null;
}
final AgentSpan span = startSpan("servlet.response");
DECORATE.afterStart(span);
span.setTag(DDTags.RESOURCE_NAME, "HttpServletResponse." + method);
// In case we lose context, inject trace into to the request.
propagate().inject(span, req, SETTER);
return activateSpan(span, true).setAsyncPropagation(true);
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void stopSpan(
@Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable throwable) {
if (scope == null) {
return;
}
DECORATE.onError(scope, throwable);
DECORATE.beforeFinish(scope);
scope.close();
}
}
}

View File

@ -0,0 +1,105 @@
import datadog.trace.agent.test.AgentTestRunner
import javax.servlet.Filter
import javax.servlet.FilterChain
import javax.servlet.FilterConfig
import javax.servlet.ServletException
import javax.servlet.ServletRequest
import javax.servlet.ServletResponse
import static datadog.trace.agent.test.utils.TraceUtils.basicSpan
import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace
class FilterTest extends AgentTestRunner {
static {
System.setProperty("dd.integration.servlet.enabled", "true")
}
def "test doFilter no-parent"() {
when:
filter.doFilter(null, null, null)
then:
assertTraces(0) {}
where:
filter = new TestFilter()
}
def "test doFilter with parent"() {
when:
runUnderTrace("parent") {
filter.doFilter(null, null, null)
}
then:
assertTraces(1) {
trace(0, 2) {
basicSpan(it, 0, "parent")
span(1) {
operationName "servlet.filter"
resourceName "${filter.class.simpleName}.doFilter"
childOf span(0)
tags {
"component" "java-web-servlet-filter"
defaultTags()
}
}
}
}
where:
filter << [new TestFilter(), new TestFilter() {}]
}
def "test doFilter exception"() {
setup:
def ex = new Exception("some error")
def filter = new TestFilter() {
@Override
void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) {
throw ex
}
}
when:
runUnderTrace("parent") {
filter.doFilter(null, null, null)
}
then:
def th = thrown(Exception)
th == ex
assertTraces(1) {
trace(0, 2) {
basicSpan(it, 0, "parent", null, ex)
span(1) {
operationName "servlet.filter"
resourceName "${filter.class.simpleName}.doFilter"
childOf span(0)
errored true
tags {
"component" "java-web-servlet-filter"
defaultTags()
errorTags(ex.class, ex.message)
}
}
}
}
}
static class TestFilter implements Filter {
@Override
void init(FilterConfig filterConfig) throws ServletException {
}
@Override
void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
}
@Override
void destroy() {
}
}
}

View File

@ -0,0 +1,275 @@
import datadog.trace.agent.test.AgentTestRunner
import groovy.servlet.AbstractHttpServlet
import javax.servlet.ServletOutputStream
import javax.servlet.http.Cookie
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import spock.lang.Subject
import static datadog.trace.agent.test.utils.TraceUtils.basicSpan
import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace
class HttpServletResponseTest extends AgentTestRunner {
static {
System.setProperty("dd.integration.servlet.enabled", "true")
}
@Subject
def response = new TestResponse()
def request = Mock(HttpServletRequest) {
getMethod() >> "GET"
getProtocol() >> "TEST"
}
def setup() {
def servlet = new AbstractHttpServlet() {}
// We need to call service so HttpServletAdvice can link the request to the response.
servlet.service(request, response)
}
def "test send no-parent"() {
when:
response.sendError(0)
response.sendError(0, "")
response.sendRedirect("")
then:
assertTraces(0) {}
}
def "test send with parent"() {
when:
runUnderTrace("parent") {
response.sendError(0)
response.sendError(0, "")
response.sendRedirect("")
}
then:
assertTraces(1) {
trace(0, 4) {
basicSpan(it, 0, "parent")
span(1) {
operationName "servlet.response"
resourceName "HttpServletResponse.sendRedirect"
childOf span(0)
tags {
"component" "java-web-servlet-response"
defaultTags()
}
}
span(2) {
operationName "servlet.response"
resourceName "HttpServletResponse.sendError"
childOf span(0)
tags {
"component" "java-web-servlet-response"
defaultTags()
}
}
span(3) {
operationName "servlet.response"
resourceName "HttpServletResponse.sendError"
childOf span(0)
tags {
"component" "java-web-servlet-response"
defaultTags()
}
}
}
}
}
def "test send with exception"() {
setup:
def ex = new Exception("some error")
def response = new TestResponse() {
@Override
void sendRedirect(String s) {
throw ex
}
}
def servlet = new AbstractHttpServlet() {}
// We need to call service so HttpServletAdvice can link the request to the response.
servlet.service(request, response)
when:
runUnderTrace("parent") {
response.sendRedirect("")
}
then:
def th = thrown(Exception)
th == ex
assertTraces(1) {
trace(0, 2) {
basicSpan(it, 0, "parent", null, ex)
span(1) {
operationName "servlet.response"
resourceName "HttpServletResponse.sendRedirect"
childOf span(0)
errored true
tags {
"component" "java-web-servlet-response"
defaultTags()
errorTags(ex.class, ex.message)
}
}
}
}
}
static class TestResponse implements HttpServletResponse {
@Override
void addCookie(Cookie cookie) {
}
@Override
boolean containsHeader(String s) {
return false
}
@Override
String encodeURL(String s) {
return null
}
@Override
String encodeRedirectURL(String s) {
return null
}
@Override
String encodeUrl(String s) {
return null
}
@Override
String encodeRedirectUrl(String s) {
return null
}
@Override
void sendError(int i, String s) throws IOException {
}
@Override
void sendError(int i) throws IOException {
}
@Override
void sendRedirect(String s) throws IOException {
}
@Override
void setDateHeader(String s, long l) {
}
@Override
void addDateHeader(String s, long l) {
}
@Override
void setHeader(String s, String s1) {
}
@Override
void addHeader(String s, String s1) {
}
@Override
void setIntHeader(String s, int i) {
}
@Override
void addIntHeader(String s, int i) {
}
@Override
void setStatus(int i) {
}
@Override
void setStatus(int i, String s) {
}
@Override
String getCharacterEncoding() {
return null
}
@Override
ServletOutputStream getOutputStream() throws IOException {
return null
}
@Override
PrintWriter getWriter() throws IOException {
return null
}
@Override
void setContentLength(int i) {
}
@Override
void setContentType(String s) {
}
@Override
void setBufferSize(int i) {
}
@Override
int getBufferSize() {
return 0
}
@Override
void flushBuffer() throws IOException {
}
@Override
void resetBuffer() {
}
@Override
boolean isCommitted() {
return false
}
@Override
void reset() {
}
@Override
void setLocale(Locale locale) {
}
@Override
Locale getLocale() {
return null
}
}
}

View File

@ -0,0 +1,125 @@
import datadog.trace.agent.test.AgentTestRunner
import groovy.servlet.AbstractHttpServlet
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import static datadog.trace.agent.test.utils.TraceUtils.basicSpan
import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace
class HttpServletTest extends AgentTestRunner {
static {
System.setProperty("dd.integration.servlet.enabled", "true")
}
def req = Mock(HttpServletRequest) {
getMethod() >> "GET"
getProtocol() >> "TEST"
}
def resp = Mock(HttpServletResponse)
def "test service no-parent"() {
when:
servlet.service(req, resp)
then:
assertTraces(0) {}
where:
servlet = new TestServlet()
}
def "test service with parent"() {
when:
runUnderTrace("parent") {
servlet.service(req, resp)
}
then:
assertTraces(1) {
trace(0, 3) {
basicSpan(it, 0, "parent")
span(1) {
operationName "servlet.service"
resourceName "HttpServlet.service"
childOf span(0)
tags {
"component" "java-web-servlet-service"
defaultTags()
}
}
span(2) {
operationName "servlet.doGet"
resourceName "${expectedResourceName}.doGet"
childOf span(1)
tags {
"component" "java-web-servlet-service"
defaultTags()
}
}
}
}
where:
servlet << [new TestServlet(), new TestServlet() {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
}
}]
expectedResourceName = servlet.class.anonymousClass ? servlet.class.name : servlet.class.simpleName
}
def "test service exception"() {
setup:
def ex = new Exception("some error")
def servlet = new TestServlet() {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
throw ex
}
}
when:
runUnderTrace("parent") {
servlet.service(req, resp)
}
then:
def th = thrown(Exception)
th == ex
assertTraces(1) {
trace(0, 3) {
basicSpan(it, 0, "parent", null, ex)
span(1) {
operationName "servlet.service"
resourceName "HttpServlet.service"
childOf span(0)
errored true
tags {
"component" "java-web-servlet-service"
defaultTags()
errorTags(ex.class, ex.message)
}
}
span(2) {
operationName "servlet.doGet"
resourceName "${servlet.class.name}.doGet"
childOf span(1)
errored true
tags {
"component" "java-web-servlet-service"
defaultTags()
errorTags(ex.class, ex.message)
}
}
}
}
}
static class TestServlet extends AbstractHttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
}
}
}

View File

@ -0,0 +1,97 @@
import datadog.trace.agent.test.AgentTestRunner
import javax.servlet.ServletException
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import static datadog.trace.agent.test.utils.TraceUtils.basicSpan
import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace
class RequestDispatcherTest extends AgentTestRunner {
static {
System.setProperty("dd.integration.servlet.enabled", "true")
}
def dispatcher = new RequestDispatcherUtils(Mock(HttpServletRequest), Mock(HttpServletResponse))
def "test dispatch no-parent"() {
when:
dispatcher.forward("")
dispatcher.include("")
then:
assertTraces(0) {}
}
def "test dispatcher #method with parent"() {
when:
runUnderTrace("parent") {
dispatcher."$method"(target)
}
then:
assertTraces(1) {
trace(0, 2) {
basicSpan(it, 0, "parent")
span(1) {
operationName "servlet.$operation"
resourceName target
childOf span(0)
tags {
"component" "java-web-servlet-dispatcher"
defaultTags()
}
}
}
}
where:
operation | method
"forward" | "forward"
"forward" | "forwardNamed"
"include" | "include"
"include" | "includeNamed"
target = "test-$method"
}
def "test dispatcher #method exception"() {
setup:
def ex = new ServletException("some error")
def dispatcher = new RequestDispatcherUtils(Mock(HttpServletRequest), Mock(HttpServletResponse), ex)
when:
runUnderTrace("parent") {
dispatcher."$method"(target)
}
then:
def th = thrown(ServletException)
th == ex
assertTraces(1) {
trace(0, 2) {
basicSpan(it, 0, "parent", null, ex)
span(1) {
operationName "servlet.$operation"
resourceName target
childOf span(0)
errored true
tags {
"component" "java-web-servlet-dispatcher"
defaultTags()
errorTags(ex.class, ex.message)
}
}
}
}
where:
operation | method
"forward" | "forward"
"forward" | "forwardNamed"
"include" | "include"
"include" | "includeNamed"
target = "test-$method"
}
}

View File

@ -0,0 +1,181 @@
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.Set;
import javax.servlet.RequestDispatcher;
import javax.servlet.Servlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public class RequestDispatcherUtils {
private final ServletRequest req;
private final ServletResponse resp;
private final ServletException toThrow;
public RequestDispatcherUtils(final ServletRequest req, final ServletResponse resp) {
this.req = req;
this.resp = resp;
toThrow = null;
}
public RequestDispatcherUtils(
final ServletRequest req, final ServletResponse resp, final ServletException toThrow) {
this.req = req;
this.resp = resp;
this.toThrow = toThrow;
}
/* RequestDispatcher can't be visible to groovy otherwise things break, so everything is
* encapsulated in here where groovy doesn't need to access it.
*/
void forward(final String target) throws ServletException, IOException {
new TestContext().getRequestDispatcher(target).forward(req, resp);
}
void include(final String target) throws ServletException, IOException {
new TestContext().getRequestDispatcher(target).include(req, resp);
}
void forwardNamed(final String target) throws ServletException, IOException {
new TestContext().getNamedDispatcher(target).forward(req, resp);
}
void includeNamed(final String target) throws ServletException, IOException {
new TestContext().getNamedDispatcher(target).include(req, resp);
}
class TestContext implements ServletContext {
@Override
public ServletContext getContext(final String s) {
return null;
}
@Override
public int getMajorVersion() {
return 0;
}
@Override
public int getMinorVersion() {
return 0;
}
@Override
public String getMimeType(final String s) {
return null;
}
@Override
public Set getResourcePaths(final String s) {
return null;
}
@Override
public URL getResource(final String s) throws MalformedURLException {
return null;
}
@Override
public InputStream getResourceAsStream(final String s) {
return null;
}
@Override
public RequestDispatcher getRequestDispatcher(final String s) {
return new TestDispatcher();
}
@Override
public RequestDispatcher getNamedDispatcher(final String s) {
return new TestDispatcher();
}
@Override
public Servlet getServlet(final String s) throws ServletException {
return null;
}
@Override
public Enumeration getServlets() {
return null;
}
@Override
public Enumeration getServletNames() {
return null;
}
@Override
public void log(final String s) {}
@Override
public void log(final Exception e, final String s) {}
@Override
public void log(final String s, final Throwable throwable) {}
@Override
public String getRealPath(final String s) {
return null;
}
@Override
public String getServerInfo() {
return null;
}
@Override
public String getInitParameter(final String s) {
return null;
}
@Override
public Enumeration getInitParameterNames() {
return null;
}
@Override
public Object getAttribute(final String s) {
return null;
}
@Override
public Enumeration getAttributeNames() {
return null;
}
@Override
public void setAttribute(final String s, final Object o) {}
@Override
public void removeAttribute(final String s) {}
@Override
public String getServletContextName() {
return null;
}
}
class TestDispatcher implements RequestDispatcher {
@Override
public void forward(final ServletRequest servletRequest, final ServletResponse servletResponse)
throws ServletException, IOException {
if (toThrow != null) {
throw toThrow;
}
}
@Override
public void include(final ServletRequest servletRequest, final ServletResponse servletResponse)
throws ServletException, IOException {
if (toThrow != null) {
throw toThrow;
}
}
}
}