Initial spring data instrumentation
ADD
dd-java-agent/instrumentation/spring-data/spring-data.gradle
+// Modified by SignalFx
+muzzle {
+ pass {
+ group = 'org.springframework.data'
+ module = 'spring-data-commons'
+ versions = "[1.5.0.RELEASE,)"
+ extraDependency "javax.servlet:javax.servlet.api:3.0.0"
+ }
+}
+
+apply from: "${rootDir}/gradle/java.gradle"
+
+apply plugin: 'org.unbroken-dome.test-sets'
+
+testSets {
+ latestDepTest {
+ dirName = 'test'
+ }
+}
+
+dependencies {
+ compileOnly(group: 'org.springframework.data', name: 'spring-data-commons', version: '1.5.0.RELEASE')
+ compileOnly(group: 'org.springframework', name: 'spring-aop', version: '3.0.0.RELEASE')
+ compile project(':dd-java-agent:agent-tooling')
+
+ compile deps.bytebuddy
+ compile deps.opentracing
+ annotationProcessor deps.autoservice
+ implementation deps.autoservice
+
+ testCompile(project(':dd-java-agent:testing'))
+ testCompile project(':dd-java-agent:instrumentation:jdbc')
+ testCompile group: 'org.spockframework', name: 'spock-spring', version: "$versions.spock"
+ testCompile group: 'org.springframework', name: 'spring-test', version: '3.0.0.RELEASE'
+ testCompile group: 'org.springframework.data', name: 'spring-data-jpa', version: '1.0.0.RELEASE'
+ testCompile group: 'org.hsqldb', name: 'hsqldb', version: '2.0.0'
+ testCompile group: 'org.eclipse.persistence', name: 'org.eclipse.persistence.jpa', version: '2.7.4'
+ testCompile group: 'org.hibernate', name: 'hibernate-entitymanager', version: '4.3.0.Final'
+
+ latestDepTestCompile group: 'org.springframework', name: 'spring-test', version: '+'
+ latestDepTestCompile group: 'org.springframework', name: 'spring-context', version: '+'
+ latestDepTestCompile group: 'org.springframework.data', name: 'spring-data-commons', version: '+'
+ latestDepTestCompile group: 'org.springframework', name: 'spring-aop', version: '+'
+ latestDepTestCompile group: 'org.springframework.data', name: 'spring-data-jpa', version: '+'
+}

ADD
dd-java-agent/instrumentation/spring-data/src/main/java/datadog/trace/instrumentation/springdata/SpringDataDecorator.java
+// Modifed by SignalFx
+package datadog.trace.instrumentation.springdata;
+
+import datadog.trace.agent.decorator.OrmClientDecorator;
+import io.opentracing.Span;
+import java.lang.reflect.Method;
+
+public class SpringDataDecorator extends OrmClientDecorator {
+ public static final SpringDataDecorator DECORATOR = new SpringDataDecorator();
+
+ @Override
+ protected String service() {
+ return "spring-data";
+ }
+
+ @Override
+ protected String[] instrumentationNames() {
+ return new String[] {"spring-data"};
+ }
+
+ @Override
+ protected String spanType() {
+ return null;
+ }
+
+ @Override
+ protected String component() {
+ return "spring-data";
+ }
+
+ @Override
+ protected String dbType() {
+ return null;
+ }
+
+ @Override
+ protected String dbUser(final Object o) {
+ return null;
+ }
+
+ @Override
+ protected String dbInstance(final Object o) {
+ return null;
+ }
+
+ @Override
+ public String entityName(final Object entity) {
+ String name = null;
+
+ if (entity instanceof Method) {
+ final Method method = (Method) entity;
+ final Class<?> clazz = method.getDeclaringClass();
+ final String methodName = method.getName();
+ final String className = clazz.getSimpleName();
+ name = String.format("%s.%s", className, methodName);
+ }
+
+ return name;
+ }
+
+ @Override
+ public Span onOperation(final Span span, final Object entity) {
+
+ assert span != null;
+ if (entity != null) {
+ final String name = entityName(entity);
+ if (name != null) {
+ span.setOperationName(name);
+ }
+ }
+ return span;
+ }
+}

ADD
dd-java-agent/instrumentation/spring-data/src/main/java/datadog/trace/instrumentation/springdata/SpringDataInstrumentation.java
+// Modified by SignalFx
+package datadog.trace.instrumentation.springdata;
+
+import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType;
+import static datadog.trace.instrumentation.springdata.SpringDataDecorator.DECORATOR;
+import static net.bytebuddy.matcher.ElementMatchers.isInterface;
+import static net.bytebuddy.matcher.ElementMatchers.isMethod;
+import static net.bytebuddy.matcher.ElementMatchers.isPublic;
+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 io.opentracing.Scope;
+import io.opentracing.Span;
+import io.opentracing.util.GlobalTracer;
+import java.lang.reflect.Method;
+import java.util.Collections;
+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 SpringDataInstrumentation extends Instrumenter.Default {
+
+ public SpringDataInstrumentation() {
+ super("spring-data");
+ }
+
+ @Override
+ public ElementMatcher<TypeDescription> typeMatcher() {
+ return not(isInterface())
+ .and(safeHasSuperType(named("org.springframework.data.repository.Repository")));
+ }
+
+ @Override
+ public String[] helperClassNames() {
+ return new String[] {
+ "datadog.trace.agent.decorator.BaseDecorator",
+ "datadog.trace.agent.decorator.ClientDecorator",
+ "datadog.trace.agent.decorator.DatabaseClientDecorator",
+ "datadog.trace.agent.decorator.OrmClientDecorator",
+ packageName + ".SpringDataDecorator"
+ };
+ }
+
+ @Override
+ public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
+ return Collections.singletonMap(isMethod().and(isPublic()), RepositoryAdvice.class.getName());
+ }
+
+ public static class RepositoryAdvice {
+
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static Scope startSpan(@Advice.Origin final Method method) {
+ final Scope scope = GlobalTracer.get().buildSpan("repository.query").startActive(true);
+ final Span span = scope.span();
+ DECORATOR.afterStart(span);
+ DECORATOR.onOperation(span, method);
+ return scope;
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void stopSpan(
+ @Advice.Enter final Scope scope, @Advice.Thrown final Throwable throwable) {
+ DECORATOR.onError(scope, throwable);
+ DECORATOR.beforeFinish(scope);
+ scope.close();
+ }
+ }
+}

ADD
dd-java-agent/instrumentation/spring-data/src/main/java/datadog/trace/instrumentation/springdata/SpringDataProxyInstrumentation.java
+// Modified by SignalFx
+package datadog.trace.instrumentation.springdata;
+
+import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType;
+import static datadog.trace.instrumentation.springdata.SpringDataDecorator.DECORATOR;
+import static net.bytebuddy.matcher.ElementMatchers.isInterface;
+import static net.bytebuddy.matcher.ElementMatchers.isMethod;
+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 static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+import com.google.auto.service.AutoService;
+import datadog.trace.agent.tooling.Instrumenter;
+import io.opentracing.Scope;
+import io.opentracing.Span;
+import io.opentracing.util.GlobalTracer;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Collections;
+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;
+import org.springframework.data.repository.Repository;
+
+@AutoService(Instrumenter.class)
+public final class SpringDataProxyInstrumentation extends Instrumenter.Default {
+
+ public SpringDataProxyInstrumentation() {
+ super("spring-data");
+ }
+
+ @Override
+ public ElementMatcher<TypeDescription> typeMatcher() {
+ return not(isInterface())
+ .and(safeHasSuperType(named("org.springframework.aop.framework.JdkDynamicAopProxy")));
+ }
+
+ @Override
+ public String[] helperClassNames() {
+ return new String[] {
+ "datadog.trace.agent.decorator.BaseDecorator",
+ "datadog.trace.agent.decorator.ClientDecorator",
+ "datadog.trace.agent.decorator.DatabaseClientDecorator",
+ "datadog.trace.agent.decorator.OrmClientDecorator",
+ packageName + ".SpringDataDecorator"
+ };
+ }
+
+ @Override
+ public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
+ return Collections.singletonMap(
+ isMethod()
+ .and(isPublic())
+ .and(named("invoke"))
+ .and(
+ takesArguments(3)
+ .and(takesArgument(0, Object.class))
+ .and(takesArgument(1, Method.class))
+ .and(takesArgument(2, Object[].class))),
+ ProxyAdvice.class.getName());
+ }
+
+ public static class ProxyAdvice {
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static Scope startSpan(
+ @Advice.Argument(value = 1, readOnly = false) Method invokedMethod) {
+ final Class<?> clazz = invokedMethod.getDeclaringClass();
+ // getting a proxy's target during type matching doesn't appear possible,
+ // so we manually check the target method's class here. As more spring functionality is
+ // added, this should be broken out to ensure it's only done once to avoid unnecessary
+ // overhead
+ if (!Repository.class.isAssignableFrom(clazz)) {
+ return null;
+ }
+
+ boolean isPublic = Modifier.isPublic(invokedMethod.getModifiers());
+ if (!isPublic) {
+ return null;
+ }
+
+ final Scope scope = GlobalTracer.get().buildSpan("proxy.query").startActive(true);
+ final Span span = scope.span();
+ DECORATOR.afterStart(span);
+ DECORATOR.onOperation(span, invokedMethod);
+ return scope;
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void stopSpan(
+ @Advice.Enter final Scope scope, @Advice.Thrown final Throwable throwable) {
+ if (scope == null) {
+ return;
+ }
+ DECORATOR.onError(scope, throwable);
+ DECORATOR.beforeFinish(scope);
+ scope.close();
+ }
+ }
+}

ADD
dd-java-agent/instrumentation/spring-data/src/test/groovy/SpringJpaTest.groovy
+// Modified by SignalFx
+import datadog.trace.agent.test.AgentTestRunner
+import io.opentracing.tag.Tags
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
+import spock.lang.Shared
+import spring.Customer
+import spring.CustomerRepository
+import spring.PersistenceConfig
+
+
+class SpringJpaTest extends AgentTestRunner {
+
+ @Shared
+ def context = new AnnotationConfigApplicationContext(PersistenceConfig)
+
+ @Shared
+ def repo = context.getBean(CustomerRepository)
+
+ def "test CRUD"() {
+ setup:
+ def customer = new Customer("Bob", "Anonymous")
+
+ expect:
+ customer.id == null
+ !repo.findAll().iterator().hasNext()
+
+ assertTraces(1) {
+ trace(0, 4) {
+ span(0) {
+ operationName "CrudRepository.findAll"
+ serviceName "hsqldb"
+ errored false
+ tags {
+ "$Tags.COMPONENT.key" "spring-data"
+ "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
+ defaultTags()
+ }
+ }
+ span(1) {
+ operationName "SimpleJpaRepository.findAll"
+ serviceName "hsqldb"
+ childOf(span(0))
+ errored false
+ tags {
+ "$Tags.COMPONENT.key" "spring-data"
+ "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
+ defaultTags()
+ }
+ }
+ span(2) {
+ serviceName "hsqldb"
+ spanType "sql"
+ childOf(span(1))
+ }
+ span(3) {
+ serviceName "hsqldb"
+ spanType "sql"
+ childOf(span(2))
+ }
+ }
+ }
+ TEST_WRITER.clear()
+
+ when:
+ repo.save(customer)
+ def savedId = customer.id
+
+ then:
+ customer.id != null
+ assertTraces(1) {
+ trace(0, 4) {
+ span(0) {
+ operationName "CrudRepository.save"
+ serviceName "hsqldb"
+ errored false
+ tags {
+ "$Tags.COMPONENT.key" "spring-data"
+ "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
+ defaultTags()
+ }
+ }
+ span(1) {
+ operationName "SimpleJpaRepository.save"
+ serviceName "hsqldb"
+ childOf(span(0))
+ errored false
+ tags {
+ "$Tags.COMPONENT.key" "spring-data"
+ "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
+ defaultTags()
+ }
+ }
+ span(2) {
+ serviceName "hsqldb"
+ spanType "sql"
+ childOf(span(1))
+ }
+ span(3) {
+ serviceName "hsqldb"
+ spanType "sql"
+ childOf(span(2))
+ }
+ }
+ }
+ TEST_WRITER.clear()
+
+ when:
+ customer.firstName = "Bill"
+ repo.save(customer)
+
+ then:
+ customer.id == savedId
+ assertTraces(1) {
+ trace(0, 5) {
+ span(0) {
+ operationName "CrudRepository.save"
+ serviceName "hsqldb"
+ errored false
+ tags {
+ "$Tags.COMPONENT.key" "spring-data"
+ "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
+ defaultTags()
+ }
+ }
+ span(1) {
+ serviceName "hsqldb"
+ spanType "sql"
+ childOf(span(0))
+ }
+ span(2) {
+ operationName "SimpleJpaRepository.save"
+ serviceName "hsqldb"
+ childOf(span(0))
+ errored false
+ tags {
+ "$Tags.COMPONENT.key" "spring-data"
+ "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
+ defaultTags()
+ }
+ }
+ span(3) {
+ serviceName "hsqldb"
+ spanType "sql"
+ childOf(span(2))
+ }
+ span(4) {
+ serviceName "hsqldb"
+ spanType "sql"
+ childOf(span(3))
+ }
+ }
+ }
+ TEST_WRITER.clear()
+
+ when:
+ customer = repo.findByLastName("Anonymous")[0]
+
+ then:
+ customer.id == savedId
+ customer.firstName == "Bill"
+ assertTraces(1) {
+ trace(0, 3) {
+ span(0) {
+ operationName "CustomerRepository.findByLastName"
+ serviceName "hsqldb"
+ errored false
+ tags {
+ "$Tags.COMPONENT.key" "spring-data"
+ "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
+ defaultTags()
+ }
+ }
+ span(1) {
+ serviceName "hsqldb"
+ spanType "sql"
+ childOf(span(0))
+ }
+ span(2) {
+ serviceName "hsqldb"
+ spanType "sql"
+ childOf(span(1))
+ }
+ }
+ TEST_WRITER.clear()
+
+ when:
+ repo.delete(customer)
+
+ then:
+ assertTraces(1) {
+ trace(0, 5) {
+ span(0) {
+ operationName "CrudRepository.delete"
+ serviceName "hsqldb"
+ errored false
+ tags {
+ "$Tags.COMPONENT.key" "spring-data"
+ "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
+ defaultTags()
+ }
+ }
+ span(1) {
+ serviceName "hsqldb"
+ spanType "sql"
+ childOf(span(0))
+ }
+ span(2) {
+ operationName "SimpleJpaRepository.delete"
+ serviceName "hsqldb"
+ childOf(span(0))
+ errored false
+ tags {
+ "$Tags.COMPONENT.key" "spring-data"
+ "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
+ defaultTags()
+ }
+ }
+ span(3) {
+ serviceName "hsqldb"
+ spanType "sql"
+ childOf(span(2))
+ }
+ span(4) {
+ serviceName "hsqldb"
+ spanType "sql"
+ childOf(span(3))
+ }
+ }
+ }
+ TEST_WRITER.clear()
+ }
+ }
+}

ADD
dd-java-agent/instrumentation/spring-data/src/test/java/spring/Customer.java
+// Modified by SignalFx
+package spring;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import lombok.Data;
+
+@Data
+@Entity
+public class Customer {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private Long id;
+
+ private String firstName;
+ private String lastName;
+
+ protected Customer() {}
+
+ public Customer(final String firstName, final String lastName) {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
+ }
+}

ADD
dd-java-agent/instrumentation/spring-data/src/test/java/spring/CustomerRepository.java
+// Modified by SignalFx
+package spring;
+
+import java.util.List;
+import org.springframework.data.repository.CrudRepository;
+
+public interface CustomerRepository extends CrudRepository<Customer, Long> {
+
+ List<Customer> findByLastName(String lastName);
+}

ADD
dd-java-agent/instrumentation/spring-data/src/test/java/spring/PersistenceConfig.java
+// Modifed by SignalFx
+package spring;
+
+import javax.sql.DataSource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.jdbc.datasource.DriverManagerDataSource;
+import org.springframework.orm.jpa.JpaTransactionManager;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.orm.jpa.vendor.Database;
+import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
+import org.springframework.transaction.PlatformTransactionManager;
+
+@EnableJpaRepositories(basePackages = "spring")
+public class PersistenceConfig {
+
+ @Bean(name = "transactionManager")
+ public PlatformTransactionManager dbTransactionManager() {
+ final JpaTransactionManager transactionManager = new JpaTransactionManager();
+ transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
+ return transactionManager;
+ }
+
+ @Bean
+ public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
+
+ final HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
+ vendorAdapter.setDatabase(Database.HSQL);
+ vendorAdapter.setGenerateDdl(true);
+
+ final LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
+ em.setDataSource(dataSource());
+ em.setPackagesToScan("spring");
+ em.setJpaVendorAdapter(vendorAdapter);
+ return em;
+ }
+
+ @Bean
+ public DataSource dataSource() {
+ final DriverManagerDataSource dataSource = new DriverManagerDataSource();
+ dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
+ dataSource.setUrl("jdbc:hsqldb:mem:test");
+ dataSource.setUsername("sa");
+ dataSource.setPassword("1");
+ return dataSource;
+ }
+}

MODIFY
settings.gradle
settings.gradle
@@ -89,6 +89,7 @@ include ':dd-java-agent:instrumentation:servlet-3'
include ':dd-java-agent:instrumentation:slf4j-mdc'
include ':dd-java-agent:instrumentation:sparkjava-2.3'
include ':dd-java-agent:instrumentation:spring-web'
+include ':dd-java-agent:instrumentation:spring-data'
include ':dd-java-agent:instrumentation:spring-webflux'
include ':dd-java-agent:instrumentation:spymemcached-2.12'
include ':dd-java-agent:instrumentation:tomcat-classloading'

Initial spring data instrumentation