Merge pull request #777 from DataDog/tyler/hibernate

Fix NPE in hibernate instrumentation and fix InstrumentationContext+Muzzle
This commit is contained in:
Tyler Benson 2019-03-25 12:27:30 -07:00 committed by GitHub
commit cfb7c19e20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 305 additions and 33 deletions

View File

@ -63,6 +63,7 @@ public interface Instrumenter {
return parentAgentBuilder;
}
final MuzzleMatcher muzzleMatcher = new MuzzleMatcher();
AgentBuilder.Identified.Extendable agentBuilder =
parentAgentBuilder
.type(
@ -73,13 +74,13 @@ public interface Instrumenter {
classLoaderMatcher(),
"Instrumentation class loader matcher unexpected exception: "
+ getClass().getName()))
.and(new MuzzleMatcher())
.and(muzzleMatcher)
.and(new PostMatchHook())
.transform(DDTransformers.defaultTransformers());
agentBuilder = injectHelperClasses(agentBuilder);
agentBuilder = contextProvider.instrumentationTransformer(agentBuilder);
agentBuilder = applyInstrumentationTransformers(agentBuilder);
agentBuilder = contextProvider.additionalInstrumentation(agentBuilder);
agentBuilder = contextProvider.additionalInstrumentation(agentBuilder, muzzleMatcher);
return agentBuilder;
}

View File

@ -70,14 +70,15 @@ import net.bytebuddy.utility.JavaModule;
@Slf4j
public class FieldBackedProvider implements InstrumentationContextProvider {
/*
Note: the value here has to be inside on of the prefixes in
datadog.trace.agent.tooling.Utils#BOOTSTRAP_PACKAGE_PREFIXES. This ensures that 'isolating' (or 'module')
classloaders like jboss and osgi see injected classes. This works because we instrument those classloaders
to load everything inside bootstrap packages.
/**
* Note: the value here has to be inside on of the prefixes in
* datadog.trace.agent.tooling.Utils#BOOTSTRAP_PACKAGE_PREFIXES. This ensures that 'isolating' (or
* 'module') classloaders like jboss and osgi see injected classes. This works because we
* instrument those classloaders to load everything inside bootstrap packages.
*/
private static final String DYNAMIC_CLASSES_PACKAGE =
"datadog.trace.bootstrap.instrumentation.context.";
private static final String INJECTED_FIELDS_MARKER_CLASS_NAME =
Utils.getInternalName(FieldBackedContextStoreAppliedMarker.class.getName());
@ -118,24 +119,24 @@ public class FieldBackedProvider implements InstrumentationContextProvider {
public AgentBuilder.Identified.Extendable instrumentationTransformer(
AgentBuilder.Identified.Extendable builder) {
if (instrumenter.contextStore().size() > 0) {
/*
Install transformer that rewrites accesses to context store with specialized bytecode that invokes appropriate
storage implementation.
/**
* Install transformer that rewrites accesses to context store with specialized bytecode that
* invokes appropriate storage implementation.
*/
builder =
builder.transform(getTransformerForASMVisitor(getContextStoreReadsRewritingVisitor()));
/*
We inject into bootstrap classloader because field accessor interfaces are needed by
context store implementations. Unfortunately this forces us to remove stored type checking
because actual classes may not be available at this point.
/**
* We inject into bootstrap classloader because field accessor interfaces are needed by
* context store implementations. Unfortunately this forces us to remove stored type checking
* because actual classes may not be available at this point.
*/
builder = builder.transform(bootstrapHelperInjector(fieldAccessorInterfaces.values()));
/*
* We inject context store implementation into bootstrap classloader because same implementation
* may be used by different instrumentations and it has to use same static map in case of
* fallback to map-backed storage.
/**
* We inject context store implementation into bootstrap classloader because same
* implementation may be used by different instrumentations and it has to use same static map
* in case of fallback to map-backed storage.
*/
builder = builder.transform(bootstrapHelperInjector(contextStoreImplementations.values()));
}
@ -329,13 +330,13 @@ public class FieldBackedProvider implements InstrumentationContextProvider {
@Override
public AgentBuilder.Identified.Extendable additionalInstrumentation(
AgentBuilder.Identified.Extendable builder) {
AgentBuilder.Identified.Extendable builder, final AgentBuilder.RawMatcher muzzleMatcher) {
if (fieldInjectionEnabled) {
for (final Map.Entry<String, String> entry : instrumenter.contextStore().entrySet()) {
/*
For each context store defined in a current instrumentation we create an agent builder
that injects necessary fields.
/**
* For each context store defined in a current instrumentation we create an agent builder
* that injects necessary fields.
*/
builder =
builder
@ -343,6 +344,13 @@ public class FieldBackedProvider implements InstrumentationContextProvider {
safeHasSuperType(named(entry.getKey())).and(not(isInterface())),
instrumenter.classLoaderMatcher())
.and(safeToInjectFieldsMatcher())
/**
* By adding the muzzleMatcher here, we are adding risk that the rules for injecting
* the classes into the classloader and the rules for adding the field to the class
* might be different. However the consequences are much greater if the class is not
* injected but the field is added, since that results in a NoClassDef error.
*/
.and(muzzleMatcher)
.transform(
getTransformerForASMVisitor(
getFieldInjectionVisitor(entry.getKey(), entry.getValue())));
@ -360,12 +368,13 @@ public class FieldBackedProvider implements InstrumentationContextProvider {
final JavaModule module,
final Class<?> classBeingRedefined,
final ProtectionDomain protectionDomain) {
/*
The idea here is that we can add fields if class is just being loaded (classBeingRedefined == null)
and we have to add same fields again if class we added fields before is being transformed again.
Note: here we assume that Class#getInterfaces() returns list of interfaces defined immediately on a
given class, not inherited from its parents. It looks like current JVM implementation does exactly
this but javadoc is not explicit about that.
/**
* The idea here is that we can add fields if class is just being loaded
* (classBeingRedefined == null) and we have to add same fields again if class we added
* fields before is being transformed again. Note: here we assume that Class#getInterfaces()
* returns list of interfaces defined immediately on a given class, not inherited from its
* parents. It looks like current JVM implementation does exactly this but javadoc is not
* explicit about that.
*/
return classBeingRedefined == null
|| Arrays.asList(classBeingRedefined.getInterfaces())

View File

@ -13,5 +13,5 @@ public interface InstrumentationContextProvider {
/** Hook to define additional instrumentation. Run at instrumentation advice is hooked up. */
AgentBuilder.Identified.Extendable additionalInstrumentation(
AgentBuilder.Identified.Extendable builder);
AgentBuilder.Identified.Extendable builder, final AgentBuilder.RawMatcher muzzleMatcher);
}

View File

@ -44,6 +44,9 @@ dependencies {
testCompile project(':dd-java-agent:testing')
testCompile project(':dd-java-agent:instrumentation:jdbc')
// Added to ensure cross compatibility:
testCompile project(':dd-java-agent:instrumentation:hibernate:core-4.0')
testCompile project(':dd-java-agent:instrumentation:hibernate:core-4.3')
testCompile group: 'org.hibernate', name: 'hibernate-core', version: '3.3.0.GA'
testCompile group: 'org.hibernate', name: 'hibernate-annotations', version: '+'

View File

@ -61,7 +61,9 @@ public class QueryInstrumentation extends AbstractHibernateInstrumentation {
final SessionState state =
SessionMethodUtils.startScopeFrom(
contextStore, query, "hibernate.query." + name, null, true);
if (state != null) {
DECORATOR.onStatement(state.getMethodScope().span(), query.getQueryString());
}
return state;
}

View File

@ -37,7 +37,9 @@ dependencies {
testCompile project(':dd-java-agent:testing')
testCompile project(':dd-java-agent:instrumentation:jdbc')
// Added to ensure cross compatibility:
testCompile project(':dd-java-agent:instrumentation:hibernate:core-3.3')
testCompile project(':dd-java-agent:instrumentation:hibernate:core-4.3')
testCompile group: 'org.hibernate', name: 'hibernate-core', version: '4.0.0.Final'
testCompile group: 'com.h2database', name: 'h2', version: '1.4.197'

View File

@ -61,7 +61,9 @@ public class QueryInstrumentation extends AbstractHibernateInstrumentation {
final SessionState state =
SessionMethodUtils.startScopeFrom(
contextStore, query, "hibernate.query." + name, null, true);
if (state != null) {
DECORATOR.onStatement(state.getMethodScope().span(), query.getQueryString());
}
return state;
}

View File

@ -37,11 +37,17 @@ dependencies {
testCompile project(':dd-java-agent:testing')
testCompile project(':dd-java-agent:instrumentation:jdbc')
// Added to ensure cross compatibility:
testCompile project(':dd-java-agent:instrumentation:hibernate:core-3.3')
testCompile project(':dd-java-agent:instrumentation:hibernate:core-4.0')
testCompile group: 'org.hibernate', name: 'hibernate-core', version: '4.3.0.Final'
testCompile group: 'org.hibernate', name: 'hibernate-entitymanager', version: '4.3.0.Final'
testCompile group: 'org.hsqldb', name: 'hsqldb', version: '2.0.0'
testCompile group: 'org.springframework.data', name: 'spring-data-jpa', version: '1.5.1.RELEASE'
latestDepTestCompile group: 'org.hibernate', name: 'hibernate-core', version: '+'
latestDepTestCompile group: 'org.hibernate', name: 'hibernate-entitymanager', version: '+'
latestDepTestCompile group: 'org.hsqldb', name: 'hsqldb', version: '2.0.0'
latestDepTestCompile group: 'org.springframework.data', name: 'spring-data-jpa', version: '+'
}

View File

@ -0,0 +1,147 @@
import datadog.trace.agent.test.AgentTestRunner
import org.springframework.context.annotation.AnnotationConfigApplicationContext
import spock.lang.Shared
import spring.Customer
import spring.CustomerRepository
import spring.PersistenceConfig
/**
* Unfortunately this test verifies that our hibernate instrumentation doesn't currently work with Spring Data Repositories.
*/
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, 2) {
span(0) {
serviceName "hsqldb"
spanType "sql"
}
span(1) {
serviceName "hsqldb"
spanType "sql"
childOf(span(0))
}
}
}
TEST_WRITER.clear()
when:
repo.save(customer)
def savedId = customer.id
then:
customer.id != null
// Behavior changed in new version:
def extraTrace = TEST_WRITER.size() == 2
assertTraces(extraTrace ? 2 : 1) {
trace(0, 2) {
span(0) {
serviceName "hsqldb"
spanType "sql"
}
span(1) {
serviceName "hsqldb"
spanType "sql"
childOf(span(0))
}
}
if (extraTrace) {
trace(1, 1) {
span(0) {
serviceName "hsqldb"
spanType "sql"
}
}
}
}
TEST_WRITER.clear()
when:
customer.firstName = "Bill"
repo.save(customer)
then:
customer.id == savedId
assertTraces(2) {
trace(0, 2) {
span(0) {
serviceName "hsqldb"
spanType "sql"
}
span(1) {
serviceName "hsqldb"
spanType "sql"
childOf(span(0))
}
}
trace(1, 1) {
span(0) {
serviceName "hsqldb"
spanType "sql"
}
}
}
TEST_WRITER.clear()
when:
customer = repo.findByLastName("Anonymous")[0]
then:
customer.id == savedId
customer.firstName == "Bill"
assertTraces(1) {
trace(0, 2) {
span(0) {
serviceName "hsqldb"
spanType "sql"
}
span(1) {
serviceName "hsqldb"
spanType "sql"
childOf(span(0))
}
}
}
TEST_WRITER.clear()
when:
repo.delete(customer)
then:
assertTraces(2) {
trace(0, 2) {
span(0) {
serviceName "hsqldb"
spanType "sql"
}
span(1) {
serviceName "hsqldb"
spanType "sql"
childOf(span(0))
}
}
trace(1, 1) {
span(0) {
serviceName "hsqldb"
spanType "sql"
}
}
}
TEST_WRITER.clear()
}
}

View File

@ -0,0 +1,31 @@
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);
}
}

View File

@ -0,0 +1,9 @@
package spring;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
List<Customer> findByLastName(String lastName);
}

View File

@ -0,0 +1,60 @@
package spring;
import java.util.Properties;
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);
em.setJpaProperties(additionalProperties());
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;
}
private Properties additionalProperties() {
final Properties properties = new Properties();
properties.setProperty("hibernate.show_sql", "true");
properties.setProperty("hibernate.hbm2ddl.auto", "create");
properties.setProperty("hibernate.dialect", "org.hibernate.dialect.HSQLDialect");
// properties.setProperty(
// "hibernate.format_sql",
// env.getProperty("spring.jpa.properties.hibernate.format_sql"));
return properties;
}
}