Skip to content

Commit 17de05e

Browse files
authored
Merge pull request #123 from DataDog/tyler/classloader-inject-helpers
Inject helper classes into application’s classloader
2 parents 8e70687 + 62c167d commit 17de05e

12 files changed

Lines changed: 302 additions & 172 deletions

File tree

dd-java-agent-ittests/src/test/java/com/datadoghq/trace/agent/test/integration/ApacheHTTPClientTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ public class ApacheHTTPClientTest {
99

1010
@Test
1111
public void test() throws Exception {
12+
// Since the HttpClientBuilder initializer doesn't work, invoke manually.
13+
Class.forName("com.datadoghq.trace.agent.InstrumentationRulesManager")
14+
.getMethod("registerClassLoad")
15+
.invoke(null);
1216

1317
final HttpClientBuilder builder = HttpClientBuilder.create();
1418
assertThat(builder.getClass().getSimpleName()).isEqualTo("TracingHttpClientBuilder");

dd-java-agent/dd-java-agent.gradle

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,12 @@ whitelistedInstructionClasses += whitelistedBranchClasses += [
1313
'com.datadoghq.trace.agent.*',
1414
'com.datadoghq.trace.agent.integration.*',
1515
'io.opentracing.contrib.mongo.TracingCommandListenerFactory',
16+
'io.opentracing.contrib.*',
1617
]
1718

1819
dependencies {
1920
compile project(':dd-trace')
2021
compile project(':dd-trace-annotations')
21-
compile(project(path: ':dd-java-agent:integrations:helpers', configuration: "shadow")) {
22-
transitive = false
23-
}
2422

2523
compile group: 'org.jboss.byteman', name: 'byteman', version: '4.0.0-BETA5'
2624

@@ -35,12 +33,24 @@ dependencies {
3533
testCompile group: 'org.mongodb', name: 'mongo-java-driver', version: '3.4.2'
3634
testCompile group: 'io.opentracing', name: 'opentracing-mock', version: '0.30.0'
3735

36+
testCompile(project(path: ':dd-java-agent:integrations:helpers', configuration: "shadow")) {
37+
transitive = false
38+
}
39+
3840
// Not bundled in with the agent. Usage requires being on the app's classpath (eg. Spring Boot's executable jar)
3941
compileOnly group: 'io.opentracing.contrib', name: 'opentracing-jdbc', version: '0.0.3'
4042
}
4143

4244
project(':dd-java-agent:integrations:helpers').afterEvaluate { helperProject ->
43-
project.compileJava.dependsOn helperProject.tasks.shadowJar
45+
project.processResources {
46+
from(helperProject.tasks.shadowJar)
47+
rename {
48+
it.startsWith("helpers") && it.endsWith(".jar") ?
49+
"helpers.jar.zip" :
50+
it
51+
}
52+
}
53+
project.processResources.dependsOn helperProject.tasks.shadowJar
4454
}
4555

4656
jar {

dd-java-agent/src/main/java/com/datadoghq/trace/agent/AgentRulesManager.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class AgentRulesManager {
2525
private static final String ddTraceVersion = DDTraceInfo.VERSION;
2626
private static final String ddTraceAnnotationsVersion = DDTraceAnnotationsInfo.VERSION;
2727

28-
private static final String SPRING_BOOT_RULE = "spring-boot-rule.btm";
28+
private static final String INITIALIZER_RULES = "initializer-rules.btm";
2929

3030
protected static volatile AgentRulesManager INSTANCE;
3131

@@ -61,7 +61,7 @@ public static void initialize(final Retransformer trans) {
6161

6262
INSTANCE = manager;
6363

64-
manager.loadRules(SPRING_BOOT_RULE, ClassLoader.getSystemClassLoader());
64+
manager.loadRules(INITIALIZER_RULES, ClassLoader.getSystemClassLoader());
6565
manager.traceAnnotationsManager.initialize();
6666
}
6767

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.datadoghq.trace.agent;
2+
3+
import com.google.common.collect.Maps;
4+
import java.lang.reflect.InvocationTargetException;
5+
import java.lang.reflect.Method;
6+
import java.util.Map;
7+
import java.util.zip.ZipEntry;
8+
import lombok.extern.slf4j.Slf4j;
9+
10+
@Slf4j
11+
public class ClassLoaderIntegrationInjector {
12+
private final Map<ZipEntry, byte[]> entries;
13+
private final Map<ClassLoader, Method> invocationPoints = Maps.newConcurrentMap();
14+
15+
public ClassLoaderIntegrationInjector(Map<ZipEntry, byte[]> entries) {
16+
this.entries = entries;
17+
}
18+
19+
public void inject(ClassLoader cl) {
20+
try {
21+
Method inovcationPoint = getInovcationPoint(cl);
22+
Map<ZipEntry, byte[]> toInject = Maps.newHashMap(entries);
23+
Map<ZipEntry, byte[]> injectedEntries = Maps.newHashMap();
24+
boolean successfulyAdded = true;
25+
while (!toInject.isEmpty() && successfulyAdded) {
26+
log.debug("Attempting to inject {} entries into {}", toInject.size(), cl);
27+
successfulyAdded = false;
28+
for (Map.Entry<ZipEntry, byte[]> entry : toInject.entrySet()) {
29+
String name = entry.getKey().getName();
30+
if (!name.endsWith(".class")) {
31+
continue;
32+
}
33+
byte[] bytes = entry.getValue();
34+
try {
35+
inovcationPoint.invoke(cl, bytes, 0, bytes.length);
36+
injectedEntries.put(entry.getKey(), entry.getValue());
37+
successfulyAdded = true;
38+
} catch (InvocationTargetException e) {
39+
log.debug("Error calling 'defineClass' method on {} for entry {}", cl, entry);
40+
}
41+
}
42+
toInject.keySet().removeAll(injectedEntries.keySet());
43+
}
44+
45+
} catch (NoSuchMethodException e) {
46+
log.error("Error getting 'defineClass' method from {}", cl);
47+
} catch (IllegalAccessException e) {
48+
log.error("Error accessing 'defineClass' method on {}", cl);
49+
}
50+
}
51+
52+
private Method getInovcationPoint(ClassLoader cl) throws NoSuchMethodException {
53+
if (invocationPoints.containsKey(invocationPoints)) {
54+
return invocationPoints.get(invocationPoints);
55+
}
56+
Class<?> clazz = cl.getClass();
57+
NoSuchMethodException firstException = null;
58+
while (clazz != null) {
59+
try {
60+
// defineClass is protected so we may need to check up the class hierarchy.
61+
Method invocationPoint =
62+
clazz.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
63+
invocationPoint.setAccessible(true);
64+
invocationPoints.put(cl, invocationPoint);
65+
return invocationPoint;
66+
} catch (NoSuchMethodException e) {
67+
if (firstException == null) {
68+
firstException = e;
69+
}
70+
clazz = clazz.getSuperclass();
71+
}
72+
}
73+
throw firstException;
74+
}
75+
}

dd-java-agent/src/main/java/com/datadoghq/trace/agent/InstrumentationChecker.java

Lines changed: 46 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,11 @@
33
import com.datadoghq.trace.resolver.FactoryUtils;
44
import com.fasterxml.jackson.annotation.JsonProperty;
55
import com.fasterxml.jackson.core.type.TypeReference;
6-
import java.io.File;
76
import java.lang.reflect.Method;
87
import java.util.ArrayList;
98
import java.util.Collections;
10-
import java.util.HashMap;
119
import java.util.List;
1210
import java.util.Map;
13-
import java.util.regex.Matcher;
14-
import java.util.regex.Pattern;
1511
import lombok.Data;
1612
import lombok.extern.slf4j.Slf4j;
1713

@@ -23,105 +19,23 @@
2319
public class InstrumentationChecker {
2420

2521
private static final String CONFIG_FILE = "dd-trace-supported-framework";
26-
private static InstrumentationChecker INSTANCE;
2722

2823
private final Map<String, List<ArtifactSupport>> rules;
29-
private final Map<String, String> frameworks;
30-
31-
private final ClassLoader classLoader;
3224

3325
/* For testing purpose */
3426
InstrumentationChecker(
3527
final Map<String, List<ArtifactSupport>> rules, final Map<String, String> frameworks) {
3628
this.rules = rules;
37-
this.frameworks = frameworks;
38-
this.classLoader = ClassLoader.getSystemClassLoader();
39-
INSTANCE = this;
4029
}
4130

42-
private InstrumentationChecker(final ClassLoader classLoader) {
43-
this.classLoader = classLoader;
31+
public InstrumentationChecker() {
4432
rules =
4533
FactoryUtils.loadConfigFromResource(
4634
CONFIG_FILE, new TypeReference<Map<String, List<ArtifactSupport>>>() {});
47-
frameworks = scanLoadedLibraries();
48-
}
49-
50-
/**
51-
* Return a list of unsupported rules regarding loading deps
52-
*
53-
* @return the list of unsupported rules
54-
* @param classLoader
55-
*/
56-
public static synchronized List<String> getUnsupportedRules(final ClassLoader classLoader) {
57-
58-
if (INSTANCE == null) {
59-
INSTANCE = new InstrumentationChecker(classLoader);
60-
}
61-
62-
return INSTANCE.doGetUnsupportedRules();
63-
}
64-
65-
private static Map<String, String> scanLoadedLibraries() {
66-
67-
final Map<String, String> frameworks = new HashMap<>();
68-
69-
// Scan classpath provided jars
70-
final List<File> jars = getJarFiles(System.getProperty("java.class.path"));
71-
for (final File file : jars) {
72-
73-
final String jarName = file.getName();
74-
final String version = extractJarVersion(jarName);
75-
76-
if (version != null) {
77-
78-
// Extract artifactId
79-
final String artifactId = file.getName().substring(0, jarName.indexOf(version) - 1);
80-
81-
// Store it
82-
frameworks.put(artifactId, version);
83-
}
84-
}
85-
log.debug("{} libraries found in the class-path", frameworks.size());
86-
87-
return frameworks;
88-
}
89-
90-
private static List<File> getJarFiles(final String paths) {
91-
final List<File> filesList = new ArrayList<>();
92-
for (final String path : paths.split(File.pathSeparator)) {
93-
final File file = new File(path);
94-
if (file.isDirectory()) {
95-
recurse(filesList, file);
96-
} else {
97-
if (file.getName().endsWith(".jar")) {
98-
log.trace("{} found in the classpath", file.getName());
99-
filesList.add(file);
100-
}
101-
}
102-
}
103-
return filesList;
104-
}
105-
106-
private static void recurse(final List<File> filesList, final File f) {
107-
final File[] list = f.listFiles();
108-
for (final File file : list) {
109-
getJarFiles(file.getPath());
110-
}
11135
}
11236

113-
private static String extractJarVersion(final String jarName) {
114-
115-
final Pattern versionPattern = Pattern.compile("-(\\d+\\..+)\\.jar");
116-
final Matcher matcher = versionPattern.matcher(jarName);
117-
if (matcher.find()) {
118-
return matcher.group(1);
119-
} else {
120-
return null;
121-
}
122-
}
123-
124-
private List<String> doGetUnsupportedRules() {
37+
public List<String> getUnsupportedRules(ClassLoader classLoader) {
38+
log.debug("Checking rule compatibility on classloader {}", classLoader);
12539

12640
final List<String> unsupportedRules = new ArrayList<>();
12741
for (final String rule : rules.keySet()) {
@@ -131,58 +45,54 @@ private List<String> doGetUnsupportedRules() {
13145
for (final ArtifactSupport check : rules.get(rule)) {
13246
log.debug("Checking rule {}", check);
13347

134-
boolean matched = true;
135-
for (final Map.Entry<String, String> identifier :
136-
check.identifyingPresentClasses.entrySet()) {
137-
final boolean classPresent = isClassPresent(identifier.getKey());
138-
if (!classPresent) {
139-
log.debug("Instrumentation {} not applied due to missing class {}.", rule, identifier);
140-
} else {
141-
String identifyingMethod = identifier.getValue();
142-
if (identifyingMethod != null && !identifyingMethod.isEmpty()) {
143-
Class clazz = getClassIfPresent(identifier.getKey(), classLoader);
144-
// already confirmed above the class is there.
145-
Method[] declaredMethods = clazz.getDeclaredMethods();
146-
boolean methodFound = false;
147-
for (Method m : declaredMethods) {
148-
if (m.getName().equals(identifyingMethod)) {
149-
methodFound = true;
150-
break;
48+
boolean matched =
49+
(check.identifyingPresentClasses != null
50+
&& !check.identifyingPresentClasses.entrySet().isEmpty())
51+
|| (check.identifyingMissingClasses != null
52+
&& !check.identifyingMissingClasses.isEmpty());
53+
if (check.identifyingPresentClasses != null) {
54+
for (final Map.Entry<String, String> identifier :
55+
check.identifyingPresentClasses.entrySet()) {
56+
final boolean classPresent = isClassPresent(identifier.getKey(), classLoader);
57+
if (!classPresent) {
58+
log.debug(
59+
"Instrumentation {} not applied due to missing class {}.", rule, identifier);
60+
} else {
61+
String identifyingMethod = identifier.getValue();
62+
if (identifyingMethod != null && !identifyingMethod.isEmpty()) {
63+
Class clazz = getClassIfPresent(identifier.getKey(), classLoader);
64+
// already confirmed above the class is there.
65+
Method[] declaredMethods = clazz.getDeclaredMethods();
66+
boolean methodFound = false;
67+
for (Method m : declaredMethods) {
68+
if (m.getName().equals(identifyingMethod)) {
69+
methodFound = true;
70+
break;
71+
}
72+
}
73+
if (!methodFound) {
74+
log.debug(
75+
"Instrumentation {} not applied due to missing method {}.{}",
76+
rule,
77+
identifier.getKey(),
78+
identifyingMethod);
79+
matched = false;
15180
}
152-
}
153-
if (!methodFound) {
154-
log.debug(
155-
"Instrumentation {} not applied due to missing method {}.{}",
156-
rule,
157-
identifier.getKey(),
158-
identifyingMethod);
159-
matched = false;
16081
}
16182
}
83+
matched &= classPresent;
16284
}
163-
matched &= classPresent;
164-
}
165-
for (final String identifyingClass : check.identifyingMissingClasses) {
166-
final boolean classMissing = !isClassPresent(identifyingClass);
167-
if (!classMissing) {
168-
log.debug(
169-
"Instrumentation {} not applied due to present class {}.", rule, identifyingClass);
170-
}
171-
matched &= classMissing;
17285
}
173-
174-
final boolean useVersionMatching =
175-
frameworks.containsKey(check.artifact)
176-
&& check.identifyingMissingClasses.isEmpty()
177-
&& check.identifyingPresentClasses.isEmpty();
178-
if (useVersionMatching) {
179-
// If no classes to scan, fall back on version regex.
180-
matched = Pattern.matches(check.supportedVersion, frameworks.get(check.artifact));
181-
if (!matched) {
182-
log.debug(
183-
"Library conflict: supported_version={}, actual_version={}",
184-
check.supportedVersion,
185-
frameworks.get(check.artifact));
86+
if (check.identifyingMissingClasses != null) {
87+
for (final String identifyingClass : check.identifyingMissingClasses) {
88+
final boolean classMissing = !isClassPresent(identifyingClass, classLoader);
89+
if (!classMissing) {
90+
log.debug(
91+
"Instrumentation {} not applied due to present class {}.",
92+
rule,
93+
identifyingClass);
94+
}
95+
matched &= classMissing;
18696
}
18797
}
18898

@@ -201,10 +111,6 @@ private List<String> doGetUnsupportedRules() {
201111
return unsupportedRules;
202112
}
203113

204-
private boolean isClassPresent(final String identifyingPresentClass) {
205-
return isClassPresent(identifyingPresentClass, classLoader);
206-
}
207-
208114
static boolean isClassPresent(final String identifyingPresentClass, ClassLoader classLoader) {
209115
return getClassIfPresent(identifyingPresentClass, classLoader) != null;
210116
}

0 commit comments

Comments
 (0)