Skip to content

Commit cfb7c19

Browse files
authored
Merge pull request #777 from DataDog/tyler/hibernate
Fix NPE in hibernate instrumentation and fix InstrumentationContext+Muzzle
2 parents a88ebb5 + 004c44d commit cfb7c19

File tree

12 files changed

+305
-33
lines changed

12 files changed

+305
-33
lines changed

dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Instrumenter.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public final AgentBuilder instrument(final AgentBuilder parentAgentBuilder) {
6363
return parentAgentBuilder;
6464
}
6565

66+
final MuzzleMatcher muzzleMatcher = new MuzzleMatcher();
6667
AgentBuilder.Identified.Extendable agentBuilder =
6768
parentAgentBuilder
6869
.type(
@@ -73,13 +74,13 @@ public final AgentBuilder instrument(final AgentBuilder parentAgentBuilder) {
7374
classLoaderMatcher(),
7475
"Instrumentation class loader matcher unexpected exception: "
7576
+ getClass().getName()))
76-
.and(new MuzzleMatcher())
77+
.and(muzzleMatcher)
7778
.and(new PostMatchHook())
7879
.transform(DDTransformers.defaultTransformers());
7980
agentBuilder = injectHelperClasses(agentBuilder);
8081
agentBuilder = contextProvider.instrumentationTransformer(agentBuilder);
8182
agentBuilder = applyInstrumentationTransformers(agentBuilder);
82-
agentBuilder = contextProvider.additionalInstrumentation(agentBuilder);
83+
agentBuilder = contextProvider.additionalInstrumentation(agentBuilder, muzzleMatcher);
8384
return agentBuilder;
8485
}
8586

dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/context/FieldBackedProvider.java

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,15 @@
7070
@Slf4j
7171
public class FieldBackedProvider implements InstrumentationContextProvider {
7272

73-
/*
74-
Note: the value here has to be inside on of the prefixes in
75-
datadog.trace.agent.tooling.Utils#BOOTSTRAP_PACKAGE_PREFIXES. This ensures that 'isolating' (or 'module')
76-
classloaders like jboss and osgi see injected classes. This works because we instrument those classloaders
77-
to load everything inside bootstrap packages.
73+
/**
74+
* Note: the value here has to be inside on of the prefixes in
75+
* datadog.trace.agent.tooling.Utils#BOOTSTRAP_PACKAGE_PREFIXES. This ensures that 'isolating' (or
76+
* 'module') classloaders like jboss and osgi see injected classes. This works because we
77+
* instrument those classloaders to load everything inside bootstrap packages.
7878
*/
7979
private static final String DYNAMIC_CLASSES_PACKAGE =
8080
"datadog.trace.bootstrap.instrumentation.context.";
81+
8182
private static final String INJECTED_FIELDS_MARKER_CLASS_NAME =
8283
Utils.getInternalName(FieldBackedContextStoreAppliedMarker.class.getName());
8384

@@ -118,24 +119,24 @@ public FieldBackedProvider(final Instrumenter.Default instrumenter) {
118119
public AgentBuilder.Identified.Extendable instrumentationTransformer(
119120
AgentBuilder.Identified.Extendable builder) {
120121
if (instrumenter.contextStore().size() > 0) {
121-
/*
122-
Install transformer that rewrites accesses to context store with specialized bytecode that invokes appropriate
123-
storage implementation.
122+
/**
123+
* Install transformer that rewrites accesses to context store with specialized bytecode that
124+
* invokes appropriate storage implementation.
124125
*/
125126
builder =
126127
builder.transform(getTransformerForASMVisitor(getContextStoreReadsRewritingVisitor()));
127128

128-
/*
129-
We inject into bootstrap classloader because field accessor interfaces are needed by
130-
context store implementations. Unfortunately this forces us to remove stored type checking
131-
because actual classes may not be available at this point.
129+
/**
130+
* We inject into bootstrap classloader because field accessor interfaces are needed by
131+
* context store implementations. Unfortunately this forces us to remove stored type checking
132+
* because actual classes may not be available at this point.
132133
*/
133134
builder = builder.transform(bootstrapHelperInjector(fieldAccessorInterfaces.values()));
134135

135-
/*
136-
* We inject context store implementation into bootstrap classloader because same implementation
137-
* may be used by different instrumentations and it has to use same static map in case of
138-
* fallback to map-backed storage.
136+
/**
137+
* We inject context store implementation into bootstrap classloader because same
138+
* implementation may be used by different instrumentations and it has to use same static map
139+
* in case of fallback to map-backed storage.
139140
*/
140141
builder = builder.transform(bootstrapHelperInjector(contextStoreImplementations.values()));
141142
}
@@ -329,20 +330,27 @@ public void visitLdcInsn(final Object value) {
329330

330331
@Override
331332
public AgentBuilder.Identified.Extendable additionalInstrumentation(
332-
AgentBuilder.Identified.Extendable builder) {
333+
AgentBuilder.Identified.Extendable builder, final AgentBuilder.RawMatcher muzzleMatcher) {
333334

334335
if (fieldInjectionEnabled) {
335336
for (final Map.Entry<String, String> entry : instrumenter.contextStore().entrySet()) {
336-
/*
337-
For each context store defined in a current instrumentation we create an agent builder
338-
that injects necessary fields.
337+
/**
338+
* For each context store defined in a current instrumentation we create an agent builder
339+
* that injects necessary fields.
339340
*/
340341
builder =
341342
builder
342343
.type(
343344
safeHasSuperType(named(entry.getKey())).and(not(isInterface())),
344345
instrumenter.classLoaderMatcher())
345346
.and(safeToInjectFieldsMatcher())
347+
/**
348+
* By adding the muzzleMatcher here, we are adding risk that the rules for injecting
349+
* the classes into the classloader and the rules for adding the field to the class
350+
* might be different. However the consequences are much greater if the class is not
351+
* injected but the field is added, since that results in a NoClassDef error.
352+
*/
353+
.and(muzzleMatcher)
346354
.transform(
347355
getTransformerForASMVisitor(
348356
getFieldInjectionVisitor(entry.getKey(), entry.getValue())));
@@ -360,13 +368,14 @@ public boolean matches(
360368
final JavaModule module,
361369
final Class<?> classBeingRedefined,
362370
final ProtectionDomain protectionDomain) {
363-
/*
364-
The idea here is that we can add fields if class is just being loaded (classBeingRedefined == null)
365-
and we have to add same fields again if class we added fields before is being transformed again.
366-
Note: here we assume that Class#getInterfaces() returns list of interfaces defined immediately on a
367-
given class, not inherited from its parents. It looks like current JVM implementation does exactly
368-
this but javadoc is not explicit about that.
369-
*/
371+
/**
372+
* The idea here is that we can add fields if class is just being loaded
373+
* (classBeingRedefined == null) and we have to add same fields again if class we added
374+
* fields before is being transformed again. Note: here we assume that Class#getInterfaces()
375+
* returns list of interfaces defined immediately on a given class, not inherited from its
376+
* parents. It looks like current JVM implementation does exactly this but javadoc is not
377+
* explicit about that.
378+
*/
370379
return classBeingRedefined == null
371380
|| Arrays.asList(classBeingRedefined.getInterfaces())
372381
.contains(FieldBackedContextStoreAppliedMarker.class);

dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/context/InstrumentationContextProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ AgentBuilder.Identified.Extendable instrumentationTransformer(
1313

1414
/** Hook to define additional instrumentation. Run at instrumentation advice is hooked up. */
1515
AgentBuilder.Identified.Extendable additionalInstrumentation(
16-
AgentBuilder.Identified.Extendable builder);
16+
AgentBuilder.Identified.Extendable builder, final AgentBuilder.RawMatcher muzzleMatcher);
1717
}

dd-java-agent/instrumentation/hibernate/core-3.3/core-3.3.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ dependencies {
4444

4545
testCompile project(':dd-java-agent:testing')
4646
testCompile project(':dd-java-agent:instrumentation:jdbc')
47+
// Added to ensure cross compatibility:
48+
testCompile project(':dd-java-agent:instrumentation:hibernate:core-4.0')
49+
testCompile project(':dd-java-agent:instrumentation:hibernate:core-4.3')
4750

4851
testCompile group: 'org.hibernate', name: 'hibernate-core', version: '3.3.0.GA'
4952
testCompile group: 'org.hibernate', name: 'hibernate-annotations', version: '+'

dd-java-agent/instrumentation/hibernate/core-3.3/src/main/java/datadog/trace/instrumentation/hibernate/core/v3_3/QueryInstrumentation.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ public static SessionState startMethod(
6161
final SessionState state =
6262
SessionMethodUtils.startScopeFrom(
6363
contextStore, query, "hibernate.query." + name, null, true);
64-
DECORATOR.onStatement(state.getMethodScope().span(), query.getQueryString());
64+
if (state != null) {
65+
DECORATOR.onStatement(state.getMethodScope().span(), query.getQueryString());
66+
}
6567
return state;
6668
}
6769

dd-java-agent/instrumentation/hibernate/core-4.0/core-4.0.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ dependencies {
3737

3838
testCompile project(':dd-java-agent:testing')
3939
testCompile project(':dd-java-agent:instrumentation:jdbc')
40-
40+
// Added to ensure cross compatibility:
41+
testCompile project(':dd-java-agent:instrumentation:hibernate:core-3.3')
42+
testCompile project(':dd-java-agent:instrumentation:hibernate:core-4.3')
4143

4244
testCompile group: 'org.hibernate', name: 'hibernate-core', version: '4.0.0.Final'
4345
testCompile group: 'com.h2database', name: 'h2', version: '1.4.197'

dd-java-agent/instrumentation/hibernate/core-4.0/src/main/java/datadog/trace/instrumentation/hibernate/core/v4_0/QueryInstrumentation.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ public static SessionState startMethod(
6161
final SessionState state =
6262
SessionMethodUtils.startScopeFrom(
6363
contextStore, query, "hibernate.query." + name, null, true);
64-
DECORATOR.onStatement(state.getMethodScope().span(), query.getQueryString());
64+
if (state != null) {
65+
DECORATOR.onStatement(state.getMethodScope().span(), query.getQueryString());
66+
}
6567
return state;
6668
}
6769

dd-java-agent/instrumentation/hibernate/core-4.3/core-4.3.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,17 @@ dependencies {
3737

3838
testCompile project(':dd-java-agent:testing')
3939
testCompile project(':dd-java-agent:instrumentation:jdbc')
40+
// Added to ensure cross compatibility:
41+
testCompile project(':dd-java-agent:instrumentation:hibernate:core-3.3')
4042
testCompile project(':dd-java-agent:instrumentation:hibernate:core-4.0')
4143

4244
testCompile group: 'org.hibernate', name: 'hibernate-core', version: '4.3.0.Final'
45+
testCompile group: 'org.hibernate', name: 'hibernate-entitymanager', version: '4.3.0.Final'
4346
testCompile group: 'org.hsqldb', name: 'hsqldb', version: '2.0.0'
47+
testCompile group: 'org.springframework.data', name: 'spring-data-jpa', version: '1.5.1.RELEASE'
4448

4549
latestDepTestCompile group: 'org.hibernate', name: 'hibernate-core', version: '+'
50+
latestDepTestCompile group: 'org.hibernate', name: 'hibernate-entitymanager', version: '+'
4651
latestDepTestCompile group: 'org.hsqldb', name: 'hsqldb', version: '2.0.0'
52+
latestDepTestCompile group: 'org.springframework.data', name: 'spring-data-jpa', version: '+'
4753
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import datadog.trace.agent.test.AgentTestRunner
2+
import org.springframework.context.annotation.AnnotationConfigApplicationContext
3+
import spock.lang.Shared
4+
import spring.Customer
5+
import spring.CustomerRepository
6+
import spring.PersistenceConfig
7+
8+
9+
/**
10+
* Unfortunately this test verifies that our hibernate instrumentation doesn't currently work with Spring Data Repositories.
11+
*/
12+
class SpringJpaTest extends AgentTestRunner {
13+
14+
@Shared
15+
def context = new AnnotationConfigApplicationContext(PersistenceConfig)
16+
17+
@Shared
18+
def repo = context.getBean(CustomerRepository)
19+
20+
def "test CRUD"() {
21+
setup:
22+
def customer = new Customer("Bob", "Anonymous")
23+
24+
expect:
25+
customer.id == null
26+
!repo.findAll().iterator().hasNext()
27+
28+
assertTraces(1) {
29+
trace(0, 2) {
30+
span(0) {
31+
serviceName "hsqldb"
32+
spanType "sql"
33+
}
34+
span(1) {
35+
serviceName "hsqldb"
36+
spanType "sql"
37+
childOf(span(0))
38+
}
39+
}
40+
}
41+
TEST_WRITER.clear()
42+
43+
when:
44+
repo.save(customer)
45+
def savedId = customer.id
46+
47+
then:
48+
customer.id != null
49+
// Behavior changed in new version:
50+
def extraTrace = TEST_WRITER.size() == 2
51+
assertTraces(extraTrace ? 2 : 1) {
52+
trace(0, 2) {
53+
span(0) {
54+
serviceName "hsqldb"
55+
spanType "sql"
56+
}
57+
span(1) {
58+
serviceName "hsqldb"
59+
spanType "sql"
60+
childOf(span(0))
61+
}
62+
}
63+
if (extraTrace) {
64+
trace(1, 1) {
65+
span(0) {
66+
serviceName "hsqldb"
67+
spanType "sql"
68+
}
69+
}
70+
}
71+
}
72+
TEST_WRITER.clear()
73+
74+
when:
75+
customer.firstName = "Bill"
76+
repo.save(customer)
77+
78+
then:
79+
customer.id == savedId
80+
assertTraces(2) {
81+
trace(0, 2) {
82+
span(0) {
83+
serviceName "hsqldb"
84+
spanType "sql"
85+
}
86+
span(1) {
87+
serviceName "hsqldb"
88+
spanType "sql"
89+
childOf(span(0))
90+
}
91+
}
92+
trace(1, 1) {
93+
span(0) {
94+
serviceName "hsqldb"
95+
spanType "sql"
96+
}
97+
}
98+
}
99+
TEST_WRITER.clear()
100+
101+
when:
102+
customer = repo.findByLastName("Anonymous")[0]
103+
104+
then:
105+
customer.id == savedId
106+
customer.firstName == "Bill"
107+
assertTraces(1) {
108+
trace(0, 2) {
109+
span(0) {
110+
serviceName "hsqldb"
111+
spanType "sql"
112+
}
113+
span(1) {
114+
serviceName "hsqldb"
115+
spanType "sql"
116+
childOf(span(0))
117+
}
118+
}
119+
}
120+
TEST_WRITER.clear()
121+
122+
when:
123+
repo.delete(customer)
124+
125+
then:
126+
assertTraces(2) {
127+
trace(0, 2) {
128+
span(0) {
129+
serviceName "hsqldb"
130+
spanType "sql"
131+
}
132+
span(1) {
133+
serviceName "hsqldb"
134+
spanType "sql"
135+
childOf(span(0))
136+
}
137+
}
138+
trace(1, 1) {
139+
span(0) {
140+
serviceName "hsqldb"
141+
spanType "sql"
142+
}
143+
}
144+
}
145+
TEST_WRITER.clear()
146+
}
147+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package spring;
2+
3+
import javax.persistence.Entity;
4+
import javax.persistence.GeneratedValue;
5+
import javax.persistence.GenerationType;
6+
import javax.persistence.Id;
7+
import lombok.Data;
8+
9+
@Data
10+
@Entity
11+
public class Customer {
12+
13+
@Id
14+
@GeneratedValue(strategy = GenerationType.AUTO)
15+
private Long id;
16+
17+
private String firstName;
18+
private String lastName;
19+
20+
protected Customer() {}
21+
22+
public Customer(final String firstName, final String lastName) {
23+
this.firstName = firstName;
24+
this.lastName = lastName;
25+
}
26+
27+
@Override
28+
public String toString() {
29+
return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
30+
}
31+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package spring;
2+
3+
import java.util.List;
4+
import org.springframework.data.repository.CrudRepository;
5+
6+
public interface CustomerRepository extends CrudRepository<Customer, Long> {
7+
8+
List<Customer> findByLastName(String lastName);
9+
}

0 commit comments

Comments
 (0)