fix: first attempt at sparkjava trace instrumentation
ADD
dd-java-agent/instrumentation/servlet-3/src/main/java/datadog/trace/instrumentation/servlet3/Filter3Instrumentation.java
+package datadog.trace.instrumentation.servlet3;
+
+import static datadog.trace.agent.tooling.ClassLoaderMatcher.classLoaderHasClasses;
+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.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.DDAdvice;
+import datadog.trace.agent.tooling.HelperInjector;
+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.SpanContext;
+import io.opentracing.contrib.web.servlet.filter.HttpServletRequestExtractAdapter;
+import io.opentracing.contrib.web.servlet.filter.ServletFilterSpanDecorator;
+import io.opentracing.propagation.Format;
+import io.opentracing.tag.Tags;
+import io.opentracing.util.GlobalTracer;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicBoolean;
+import javax.servlet.AsyncEvent;
+import javax.servlet.AsyncListener;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import net.bytebuddy.agent.builder.AgentBuilder;
+import net.bytebuddy.asm.Advice;
+
+@AutoService(Instrumenter.class)
+public final class Filter3Instrumentation extends Instrumenter.Configurable {
+ public static final String SERVLET_OPERATION_NAME = "servlet.request";
+
+ public Filter3Instrumentation() {
+ super("servlet", "servlet-3");
+ }
+
+ @Override
+ public AgentBuilder apply(final AgentBuilder agentBuilder) {
+ return agentBuilder
+ .type(
+ not(isInterface()).and(hasSuperType(named("javax.servlet.Filter"))),
+ classLoaderHasClasses("javax.servlet.AsyncEvent", "javax.servlet.AsyncListener"))
+ .transform(
+ new HelperInjector(
+ "io.opentracing.contrib.web.servlet.filter.HttpServletRequestExtractAdapter",
+ "io.opentracing.contrib.web.servlet.filter.HttpServletRequestExtractAdapter$MultivaluedMapFlatIterator",
+ "io.opentracing.contrib.web.servlet.filter.ServletFilterSpanDecorator",
+ "io.opentracing.contrib.web.servlet.filter.ServletFilterSpanDecorator$1",
+ "io.opentracing.contrib.web.servlet.filter.TracingFilter",
+ "io.opentracing.contrib.web.servlet.filter.TracingFilter$1",
+ FilterChain3Advice.class.getName() + "$TagSettingAsyncListener"))
+ .transform(
+ DDAdvice.create()
+ .advice(
+ named("doFilter")
+ .and(takesArgument(0, named("javax.servlet.ServletRequest")))
+ .and(takesArgument(1, named("javax.servlet.ServletResponse")))
+ .and(takesArgument(2, named("javax.servlet.ServletResponse")))
+ .and(isPublic()),
+ FilterChain3Advice.class.getName()))
+ .asDecorator();
+ }
+
+ public static class FilterChain3Advice {
+
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static Scope startSpan(@Advice.Argument(0) final ServletRequest req) {
+ if (GlobalTracer.get().activeSpan() != null || !(req instanceof HttpServletRequest)) {
+ // Tracing might already be applied by the FilterChain. If so ignore this.
+ return null;
+ }
+
+ final SpanContext extractedContext =
+ GlobalTracer.get()
+ .extract(
+ Format.Builtin.HTTP_HEADERS,
+ new HttpServletRequestExtractAdapter((HttpServletRequest) req));
+
+ final Scope scope =
+ GlobalTracer.get()
+ .buildSpan(SERVLET_OPERATION_NAME)
+ .asChildOf(extractedContext)
+ .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER)
+ .withTag(DDTags.SPAN_TYPE, DDSpanTypes.WEB_SERVLET)
+ .startActive(false);
+
+ ServletFilterSpanDecorator.STANDARD_TAGS.onRequest((HttpServletRequest) req, scope.span());
+ return scope;
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void stopSpan(
+ @Advice.Argument(0) final ServletRequest request,
+ @Advice.Argument(1) final ServletResponse response,
+ @Advice.Enter final Scope scope,
+ @Advice.Thrown final Throwable throwable) {
+
+ if (scope != null) {
+ if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
+ final HttpServletRequest req = (HttpServletRequest) request;
+ final HttpServletResponse resp = (HttpServletResponse) response;
+ final Span span = scope.span();
+
+ if (throwable != null) {
+ ServletFilterSpanDecorator.STANDARD_TAGS.onError(req, resp, throwable, span);
+ span.log(Collections.singletonMap(ERROR_OBJECT, throwable));
+ scope.close();
+ scope.span().finish(); // Finish the span manually since finishSpanOnClose was false
+ } else if (req.isAsyncStarted()) {
+ final AtomicBoolean activated = new AtomicBoolean(false);
+ // what if async is already finished? This would not be called
+ req.getAsyncContext().addListener(new TagSettingAsyncListener(activated, span));
+ } else {
+ ServletFilterSpanDecorator.STANDARD_TAGS.onResponse(req, resp, span);
+ scope.close();
+ scope.span().finish(); // Finish the span manually since finishSpanOnClose was false
+ }
+ }
+ }
+ }
+
+ public static class TagSettingAsyncListener implements AsyncListener {
+ private final AtomicBoolean activated;
+ private final Span span;
+
+ public TagSettingAsyncListener(final AtomicBoolean activated, final Span span) {
+ this.activated = activated;
+ this.span = span;
+ }
+
+ @Override
+ public void onComplete(final AsyncEvent event) throws IOException {
+ if (activated.compareAndSet(false, true)) {
+ try (Scope scope = GlobalTracer.get().scopeManager().activate(span, true)) {
+ ServletFilterSpanDecorator.STANDARD_TAGS.onResponse(
+ (HttpServletRequest) event.getSuppliedRequest(),
+ (HttpServletResponse) event.getSuppliedResponse(),
+ span);
+ }
+ }
+ }
+
+ @Override
+ public void onTimeout(final AsyncEvent event) throws IOException {
+ if (activated.compareAndSet(false, true)) {
+ try (Scope scope = GlobalTracer.get().scopeManager().activate(span, true)) {
+ ServletFilterSpanDecorator.STANDARD_TAGS.onTimeout(
+ (HttpServletRequest) event.getSuppliedRequest(),
+ (HttpServletResponse) event.getSuppliedResponse(),
+ event.getAsyncContext().getTimeout(),
+ span);
+ }
+ }
+ }
+
+ @Override
+ public void onError(final AsyncEvent event) throws IOException {
+ if (event.getThrowable() != null && activated.compareAndSet(false, true)) {
+ try (Scope scope = GlobalTracer.get().scopeManager().activate(span, true)) {
+ ServletFilterSpanDecorator.STANDARD_TAGS.onError(
+ (HttpServletRequest) event.getSuppliedRequest(),
+ (HttpServletResponse) event.getSuppliedResponse(),
+ event.getThrowable(),
+ span);
+ span.log(Collections.singletonMap(ERROR_OBJECT, event.getThrowable()));
+ }
+ }
+ }
+
+ @Override
+ public void onStartAsync(final AsyncEvent event) throws IOException {}
+ }
+ }
+}

ADD
dd-java-agent/instrumentation/sparkjava/sparkjava.gradle
+apply from: "${rootDir}/gradle/java.gradle"
+
+dependencies {
+ compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '3.1.0'
+ compile('io.opentracing.contrib:opentracing-web-servlet-filter:0.1.0') {
+ transitive = false
+ }
+
+ compile project(':dd-java-agent:agent-tooling')
+
+ 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: 'com.sparkjava', name: 'spark-core', version: '2.7.1'
+ testCompile group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.6.0'
+
+}

ADD
dd-java-agent/instrumentation/sparkjava/src/main/java/datadog/trace/instrumentation/sparkjava/Filter3Instrumentation.java
+package datadog.trace.instrumentation.servlet3;
+
+import static datadog.trace.agent.tooling.ClassLoaderMatcher.classLoaderHasClasses;
+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.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.DDAdvice;
+import datadog.trace.agent.tooling.HelperInjector;
+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.SpanContext;
+import io.opentracing.contrib.web.servlet.filter.HttpServletRequestExtractAdapter;
+import io.opentracing.contrib.web.servlet.filter.ServletFilterSpanDecorator;
+import io.opentracing.propagation.Format;
+import io.opentracing.tag.Tags;
+import io.opentracing.util.GlobalTracer;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicBoolean;
+import javax.servlet.AsyncEvent;
+import javax.servlet.AsyncListener;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import net.bytebuddy.agent.builder.AgentBuilder;
+import net.bytebuddy.asm.Advice;
+
+@AutoService(Instrumenter.class)
+public final class Filter3Instrumentation extends Instrumenter.Configurable {
+ public static final String SERVLET_OPERATION_NAME = "servlet.request";
+
+ public Filter3Instrumentation() {
+ super("servlet", "servlet-3");
+ }
+
+ @Override
+ public AgentBuilder apply(final AgentBuilder agentBuilder) {
+ return agentBuilder
+ .type(
+ not(isInterface()).and(hasSuperType(named("javax.servlet.Filter"))),
+ classLoaderHasClasses("javax.servlet.AsyncEvent", "javax.servlet.AsyncListener"))
+ .transform(
+ new HelperInjector(
+ "io.opentracing.contrib.web.servlet.filter.HttpServletRequestExtractAdapter",
+ "io.opentracing.contrib.web.servlet.filter.HttpServletRequestExtractAdapter$MultivaluedMapFlatIterator",
+ "io.opentracing.contrib.web.servlet.filter.ServletFilterSpanDecorator",
+ "io.opentracing.contrib.web.servlet.filter.ServletFilterSpanDecorator$1",
+ "io.opentracing.contrib.web.servlet.filter.TracingFilter",
+ "io.opentracing.contrib.web.servlet.filter.TracingFilter$1",
+ FilterChain3Advice.class.getName() + "$TagSettingAsyncListener"))
+ .transform(
+ DDAdvice.create()
+ .advice(
+ named("doFilter")
+ .and(takesArgument(0, named("javax.servlet.ServletRequest")))
+ .and(takesArgument(1, named("javax.servlet.ServletResponse")))
+ .and(takesArgument(2, named("javax.servlet.ServletResponse")))
+ .and(isPublic()),
+ FilterChain3Advice.class.getName()))
+ .asDecorator();
+ }
+
+ public static class FilterChain3Advice {
+
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static Scope startSpan(@Advice.Argument(0) final ServletRequest req) {
+ if (GlobalTracer.get().activeSpan() != null || !(req instanceof HttpServletRequest)) {
+ // Tracing might already be applied by the FilterChain. If so ignore this.
+ return null;
+ }
+
+ final SpanContext extractedContext =
+ GlobalTracer.get()
+ .extract(
+ Format.Builtin.HTTP_HEADERS,
+ new HttpServletRequestExtractAdapter((HttpServletRequest) req));
+
+ final Scope scope =
+ GlobalTracer.get()
+ .buildSpan(SERVLET_OPERATION_NAME)
+ .asChildOf(extractedContext)
+ .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER)
+ .withTag(DDTags.SPAN_TYPE, DDSpanTypes.WEB_SERVLET)
+ .startActive(false);
+
+ ServletFilterSpanDecorator.STANDARD_TAGS.onRequest((HttpServletRequest) req, scope.span());
+ return scope;
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void stopSpan(
+ @Advice.Argument(0) final ServletRequest request,
+ @Advice.Argument(1) final ServletResponse response,
+ @Advice.Enter final Scope scope,
+ @Advice.Thrown final Throwable throwable) {
+
+ if (scope != null) {
+ if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
+ final HttpServletRequest req = (HttpServletRequest) request;
+ final HttpServletResponse resp = (HttpServletResponse) response;
+ final Span span = scope.span();
+
+ if (throwable != null) {
+ ServletFilterSpanDecorator.STANDARD_TAGS.onError(req, resp, throwable, span);
+ span.log(Collections.singletonMap(ERROR_OBJECT, throwable));
+ scope.close();
+ scope.span().finish(); // Finish the span manually since finishSpanOnClose was false
+ } else if (req.isAsyncStarted()) {
+ final AtomicBoolean activated = new AtomicBoolean(false);
+ // what if async is already finished? This would not be called
+ req.getAsyncContext().addListener(new TagSettingAsyncListener(activated, span));
+ } else {
+ ServletFilterSpanDecorator.STANDARD_TAGS.onResponse(req, resp, span);
+ scope.close();
+ scope.span().finish(); // Finish the span manually since finishSpanOnClose was false
+ }
+ }
+ }
+ }
+
+ public static class TagSettingAsyncListener implements AsyncListener {
+ private final AtomicBoolean activated;
+ private final Span span;
+
+ public TagSettingAsyncListener(final AtomicBoolean activated, final Span span) {
+ this.activated = activated;
+ this.span = span;
+ }
+
+ @Override
+ public void onComplete(final AsyncEvent event) throws IOException {
+ if (activated.compareAndSet(false, true)) {
+ try (Scope scope = GlobalTracer.get().scopeManager().activate(span, true)) {
+ ServletFilterSpanDecorator.STANDARD_TAGS.onResponse(
+ (HttpServletRequest) event.getSuppliedRequest(),
+ (HttpServletResponse) event.getSuppliedResponse(),
+ span);
+ }
+ }
+ }
+
+ @Override
+ public void onTimeout(final AsyncEvent event) throws IOException {
+ if (activated.compareAndSet(false, true)) {
+ try (Scope scope = GlobalTracer.get().scopeManager().activate(span, true)) {
+ ServletFilterSpanDecorator.STANDARD_TAGS.onTimeout(
+ (HttpServletRequest) event.getSuppliedRequest(),
+ (HttpServletResponse) event.getSuppliedResponse(),
+ event.getAsyncContext().getTimeout(),
+ span);
+ }
+ }
+ }
+
+ @Override
+ public void onError(final AsyncEvent event) throws IOException {
+ if (event.getThrowable() != null && activated.compareAndSet(false, true)) {
+ try (Scope scope = GlobalTracer.get().scopeManager().activate(span, true)) {
+ ServletFilterSpanDecorator.STANDARD_TAGS.onError(
+ (HttpServletRequest) event.getSuppliedRequest(),
+ (HttpServletResponse) event.getSuppliedResponse(),
+ event.getThrowable(),
+ span);
+ span.log(Collections.singletonMap(ERROR_OBJECT, event.getThrowable()));
+ }
+ }
+ }
+
+ @Override
+ public void onStartAsync(final AsyncEvent event) throws IOException {}
+ }
+ }
+}

ADD
dd-java-agent/instrumentation/sparkjava/src/test/groovy/SparkJavaBasedTest.groovy
+import datadog.trace.agent.test.AgentTestRunner
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import spock.lang.Timeout
+
+
+@Timeout(5)
+class SparkJavaBasedTest extends AgentTestRunner {
+
+ private int port = 4567
+ OkHttpClient client = new OkHttpClient.Builder().build()
+
+ def "valid response"() {
+ setup:
+ def request = new Request.Builder()
+ .url("http://localhost:$port/")
+ .get()
+ .build()
+ def response = client.newCall(request).execute()
+
+ expect:
+ port != 0
+ response.body().string() == "Hello World"
+
+ and:
+ TEST_WRITER.waitForTraces(1)
+ TEST_WRITER.size() == 1
+ }
+
+}

ADD
dd-java-agent/instrumentation/sparkjava/src/test/java/TestSparkJavaApplication.java
+import spark.Spark;
+
+public class TestSparkJavaApplication {
+
+ public static void main(final String[] args) {
+
+ Spark.get("/", (req, res) -> "Hello World");
+ Spark.get("/param/:param", (req, res) -> "Hello " + req.params("param"));
+ Spark.get("/exception/:param", (req, res) -> {
+ throw new RuntimeException(req.params("param"));
+ });
+
+ }
+}

MODIFY
settings.gradle
settings.gradle
@@ -34,6 +34,7 @@ include ':dd-java-agent:instrumentation:play-2.4:play-2.6-testing'
include ':dd-java-agent:instrumentation:ratpack-1.4'
include ':dd-java-agent:instrumentation:servlet-2'
include ':dd-java-agent:instrumentation:servlet-3'
+include ':dd-java-agent:instrumentation:sparkjava'
include ':dd-java-agent:instrumentation:spring-web'
include ':dd-java-agent:instrumentation:trace-annotation'

fix: first attempt at sparkjava trace instrumentation