Skip to content

Commit 62c167d

Browse files
committed
Inject helper classes into application’s classloader
This is important for things like Spring Boot’s executable jar classloader which keeps all the jars individually rather than on the system classloader.
1 parent 8e70687 commit 62c167d

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)