Merge pull request #1165 from DataDog/tyler/servlet

Add servlet integrations for request and response dispatch back in.
This commit is contained in:
Tyler Benson 2020-01-13 13:17:29 -08:00 committed by GitHub
commit a52a1420db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1573 additions and 5 deletions

View File

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

View File

@ -0,0 +1,81 @@
package datadog.trace.instrumentation.jdbc;
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.jdbc.DataSourceDecorator.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.instrumentation.api.AgentScope;
import datadog.trace.instrumentation.api.AgentSpan;
import java.util.Map;
import javax.sql.DataSource;
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 DataSourceInstrumentation extends Instrumenter.Default {
public DataSourceInstrumentation() {
super("jdbc-datasource");
}
@Override
public boolean defaultEnabled() {
return false;
}
@Override
public String[] helperClassNames() {
return new String[] {
"datadog.trace.agent.decorator.BaseDecorator", packageName + ".DataSourceDecorator",
};
}
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return not(isInterface()).and(safeHasSuperType(named("javax.sql.DataSource")));
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(named("getConnection"), GetConnectionAdvice.class.getName());
}
public static class GetConnectionAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static AgentScope start(@Advice.This final DataSource ds) {
if (activeSpan() == null) {
// Don't want to generate a new top-level span
return null;
}
final AgentSpan span = startSpan("database.connection");
DECORATE.afterStart(span);
span.setTag(DDTags.RESOURCE_NAME, ds.getClass().getSimpleName() + ".getConnection");
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

@ -5,8 +5,10 @@ import datadog.trace.agent.test.AgentTestRunner
import datadog.trace.api.Config
import datadog.trace.api.DDSpanTypes
import datadog.trace.instrumentation.api.Tags
import org.apache.derby.jdbc.EmbeddedDataSource
import org.apache.derby.jdbc.EmbeddedDriver
import org.h2.Driver
import org.h2.jdbcx.JdbcDataSource
import org.hsqldb.jdbc.JDBCDriver
import spock.lang.Shared
import spock.lang.Unroll
@ -25,6 +27,9 @@ import static datadog.trace.agent.test.utils.TraceUtils.basicSpan
import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace
class JDBCInstrumentationTest extends AgentTestRunner {
static {
System.setProperty("dd.integration.jdbc-datasource.enabled", "true")
}
@Shared
def dbName = "jdbcUnitTest"
@ -550,6 +555,66 @@ class JDBCInstrumentationTest extends AgentTestRunner {
false | "derby" | new EmbeddedDriver() | "jdbc:derby:memory:" + dbName + ";create=true" | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1"
}
def "calling #datasource.class.simpleName getConnection generates a span when under existing trace"() {
setup:
assert datasource instanceof DataSource
init?.call(datasource)
when:
datasource.getConnection().close()
then:
!TEST_WRITER.any { it.any { it.operationName == "database.connection" } }
TEST_WRITER.clear()
when:
runUnderTrace("parent") {
datasource.getConnection().close()
}
then:
assertTraces(1) {
trace(0, recursive ? 3 : 2) {
basicSpan(it, 0, "parent")
span(1) {
operationName "database.connection"
resourceName "${datasource.class.simpleName}.getConnection"
childOf span(0)
tags {
"$Tags.COMPONENT" "java-jdbc-connection"
defaultTags()
}
}
if (recursive) {
span(2) {
operationName "database.connection"
resourceName "${datasource.class.simpleName}.getConnection"
childOf span(1)
tags {
"$Tags.COMPONENT" "java-jdbc-connection"
defaultTags()
}
}
}
}
}
where:
datasource | init
new JdbcDataSource() | { ds -> ds.setURL(jdbcUrls.get("h2")) }
new EmbeddedDataSource() | { ds -> ds.jdbcurl = jdbcUrls.get("derby") }
cpDatasources.get("hikari").get("h2") | null
cpDatasources.get("hikari").get("derby") | null
cpDatasources.get("c3p0").get("h2") | null
cpDatasources.get("c3p0").get("derby") | null
// Tomcat's pool doesn't work because the getConnection method is
// implemented in a parent class that doesn't implement DataSource
recursive = datasource instanceof EmbeddedDataSource
}
def "test getClientInfo exception"() {
setup:
Connection connection = new TestConnection(false)

View File

@ -9,6 +9,7 @@ import static datadog.trace.instrumentation.servlet2.HttpServletRequestExtractAd
import static datadog.trace.instrumentation.servlet2.Servlet2Decorator.DECORATE;
import datadog.trace.api.DDTags;
import datadog.trace.bootstrap.InstrumentationContext;
import datadog.trace.instrumentation.api.AgentScope;
import datadog.trace.instrumentation.api.AgentSpan;
import datadog.trace.instrumentation.api.Tags;
@ -36,12 +37,16 @@ public class Servlet2Advice {
return null;
}
final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
if (response instanceof HttpServletResponse) {
// For use by HttpServletResponseInstrumentation:
InstrumentationContext.get(HttpServletResponse.class, HttpServletRequest.class)
.put((HttpServletResponse) response, httpServletRequest);
response = new StatusSavingHttpServletResponseWrapper((HttpServletResponse) response);
}
final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
final AgentSpan.Context extractedContext = propagate().extract(httpServletRequest, GETTER);
final AgentSpan span =

View File

@ -49,6 +49,12 @@ public final class Servlet2Instrumentation extends Instrumenter.Default {
named("javax.servlet.FilterChain").or(named("javax.servlet.http.HttpServlet"))));
}
@Override
public Map<String, String> contextStore() {
return singletonMap(
"javax.servlet.http.HttpServletResponse", "javax.servlet.http.HttpServletRequest");
}
/**
* Here we are instrumenting the public method for HttpServlet. This should ensure that this
* advice is always called before HttpServletInstrumentation which is instrumenting the protected

View File

@ -1,7 +1,6 @@
package datadog.trace.instrumentation.servlet3;
import datadog.trace.instrumentation.api.AgentPropagation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
@ -14,7 +13,7 @@ public class HttpServletRequestExtractAdapter
@Override
public List<String> keys(final HttpServletRequest carrier) {
final ArrayList<String> keys = Collections.list(carrier.getHeaderNames());
final List<String> keys = Collections.list(carrier.getHeaderNames());
keys.addAll(Collections.list(carrier.getAttributeNames()));
return keys;
}

View File

@ -9,6 +9,7 @@ import static datadog.trace.instrumentation.servlet3.HttpServletRequestExtractAd
import static datadog.trace.instrumentation.servlet3.Servlet3Decorator.DECORATE;
import datadog.trace.api.DDTags;
import datadog.trace.bootstrap.InstrumentationContext;
import datadog.trace.instrumentation.api.AgentScope;
import datadog.trace.instrumentation.api.AgentSpan;
import datadog.trace.instrumentation.api.Tags;
@ -24,7 +25,10 @@ public class Servlet3Advice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static AgentScope onEnter(
@Advice.This final Object servlet, @Advice.Argument(0) final ServletRequest request) {
@Advice.This final Object servlet,
@Advice.Argument(0) final ServletRequest request,
@Advice.Argument(1) final ServletResponse response) {
final boolean hasActiveTrace = activeSpan() != null;
final boolean hasServletTrace = request.getAttribute(DD_SPAN_ATTRIBUTE) instanceof AgentSpan;
final boolean invalidRequest = !(request instanceof HttpServletRequest);
@ -35,6 +39,10 @@ public class Servlet3Advice {
final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// For use by HttpServletResponseInstrumentation:
InstrumentationContext.get(HttpServletResponse.class, HttpServletRequest.class)
.put((HttpServletResponse) response, httpServletRequest);
final AgentSpan.Context extractedContext = propagate().extract(httpServletRequest, GETTER);
final AgentSpan span =

View File

@ -41,6 +41,12 @@ public final class Servlet3Instrumentation extends Instrumenter.Default {
named("javax.servlet.FilterChain").or(named("javax.servlet.http.HttpServlet"))));
}
@Override
public Map<String, String> contextStore() {
return singletonMap(
"javax.servlet.http.HttpServletResponse", "javax.servlet.http.HttpServletRequest");
}
/**
* Here we are instrumenting the public method for HttpServlet. This should ensure that this
* advice is always called before HttpServletInstrumentation which is instrumenting the protected

View File

@ -1 +1,29 @@
muzzle {
pass {
group = "javax.servlet"
module = 'javax.servlet-api'
versions = "[,]"
assertInverse = true
}
pass {
group = "javax.servlet"
module = 'servlet-api'
versions = "[,]"
}
}
apply from: "${rootDir}/gradle/java.gradle"
dependencies {
compileOnly group: 'javax.servlet', name: 'servlet-api', version: '2.3'
testCompile group: 'javax.servlet', name: 'servlet-api', version: '2.3'
// servlet request instrumentation required for linking request to response.
testCompile project(':dd-java-agent:instrumentation:servlet:request-2')
// Don't want to conflict with jetty from the test server.
testCompile(project(':dd-java-agent:testing')) {
exclude group: 'org.eclipse.jetty', module: 'jetty-server'
}
}

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,113 @@
package datadog.trace.instrumentation.servlet.dispatcher;
import static datadog.trace.agent.decorator.HttpServerDecorator.DD_SPAN_ATTRIBUTE;
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 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);
// temporarily remove from request to avoid spring resource name bubbling up:
request.removeAttribute(DD_SPAN_ATTRIBUTE);
return activateSpan(span, true).setAsyncPropagation(true);
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void stop(
@Advice.Enter final AgentScope scope,
@Advice.Argument(0) final ServletRequest request,
@Advice.Thrown final Throwable throwable) {
if (scope == null) {
return;
}
// now add it back...
request.setAttribute(DD_SPAN_ATTRIBUTE, scope.span());
DECORATE.onError(scope, throwable);
DECORATE.beforeFinish(scope);
scope.close();
}
}
}

View File

@ -0,0 +1,57 @@
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 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-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-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-service"};
}
@Override
protected String spanType() {
return null;
}
@Override
protected String component() {
return "java-web-servlet-service";
}
}

View File

@ -0,0 +1,97 @@
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.instrumentation.api.AgentScope;
import datadog.trace.instrumentation.api.AgentSpan;
import java.lang.reflect.Method;
import java.util.Map;
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-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());
}
public static class HttpServletAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static AgentScope start(@Advice.Origin final Method method) {
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,100 @@
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 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,106 @@
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-filter.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,282 @@
import datadog.trace.agent.test.AgentTestRunner
import groovy.servlet.AbstractHttpServlet
import spock.lang.Subject
import javax.servlet.ServletOutputStream
import javax.servlet.ServletRequest
import javax.servlet.ServletResponse
import javax.servlet.http.Cookie
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
import static java.util.Collections.emptyEnumeration
class HttpServletResponseTest extends AgentTestRunner {
@Subject
def response = new TestResponse()
def request = Mock(HttpServletRequest) {
getMethod() >> "GET"
getProtocol() >> "TEST"
getHeaderNames() >> emptyEnumeration()
getAttributeNames() >> emptyEnumeration()
}
def setup() {
def servlet = new AbstractHttpServlet() {}
// We need to call service so HttpServletAdvice can link the request to the response.
servlet.service((ServletRequest) request, (ServletResponse) response)
assert response.__datadogContext$javax$servlet$http$HttpServletResponse != null
TEST_WRITER.clear()
}
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((ServletRequest) request, (ServletResponse) response)
assert response.__datadogContext$javax$servlet$http$HttpServletResponse != null
TEST_WRITER.clear()
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,126 @@
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-service.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,95 @@
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 {
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;
}
}
}
}