Skip to content

Commit c4a9dc6

Browse files
authored
Readded ldap plugin, added instrumentation module opening support (#2977)
1 parent 73b6262 commit c4a9dc6

File tree

19 files changed

+777
-26
lines changed

19 files changed

+777
-26
lines changed

CHANGELOG.asciidoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ endif::[]
3434
===== Features
3535
* Add experimental log sending from the agent with `log_sending` - {pull}2694[#2694]
3636
* Add bootstrap checks that enable <<jvm-filtering>> on startup - {pull}2951[#2951]
37+
* Added support for LDAP - {pull}2977[#2977]
3738
3839
[float]
3940
===== Bug fixes
40-
41-
* Use `127.0.0.1` as defaut for `server_url` to prevent ipv6 ambiguity - {pull}2927[#2927]
41+
* Use `127.0.0.1` as default for `server_url` to prevent ipv6 ambiguity - {pull}2927[#2927]
4242
* Fix some span-compression concurrency issues - {pull}2865[#2865]
4343
* Add warning when agent is accidentally started on a JVM/JDK command-line tool - {pull}2924[#2924]
4444
* Fix `NullPointerException` caused by the Elasticsearch REST client instrumentation when collecting dropped span metrics - {pull}2959[#2959]

apm-agent-core/src/main/java/co/elastic/apm/agent/bci/ElasticApmAgent.java

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import co.elastic.apm.agent.bci.bytebuddy.RootPackageCustomLocator;
3131
import co.elastic.apm.agent.bci.bytebuddy.SimpleMethodSignatureOffsetMappingFactory;
3232
import co.elastic.apm.agent.bci.classloading.ExternalPluginClassLoader;
33+
import co.elastic.apm.agent.bci.modules.ModuleOpener;
3334
import co.elastic.apm.agent.common.ThreadUtils;
3435
import co.elastic.apm.agent.common.util.SystemStandardOutputLogger;
3536
import co.elastic.apm.agent.configuration.CoreConfiguration;
@@ -83,8 +84,10 @@
8384
import java.util.ArrayList;
8485
import java.util.Collection;
8586
import java.util.Collections;
87+
import java.util.HashMap;
8688
import java.util.HashSet;
8789
import java.util.List;
90+
import java.util.Map;
8891
import java.util.Set;
8992
import java.util.concurrent.ConcurrentHashMap;
9093
import java.util.concurrent.ConcurrentMap;
@@ -132,7 +135,7 @@ public class ElasticApmAgent {
132135
* with the corresponding instrumentation class.
133136
*/
134137
private static final ConcurrentMap<String, ClassLoader> adviceClassName2instrumentationClassLoader = new ConcurrentHashMap<>();
135-
private static final ConcurrentMap<String, Collection<String>> pluginPackages2pluginClassLoaderRootPackages = new ConcurrentHashMap<>();
138+
private static final ConcurrentMap<String, PluginClassLoaderCustomizations> pluginPackages2pluginClassLoaderCustomizations = new ConcurrentHashMap<>();
136139

137140
/**
138141
* Called reflectively by {@code co.elastic.apm.agent.premain.AgentMain} to initialize the agent
@@ -252,9 +255,14 @@ private static synchronized void initInstrumentation(final ElasticApmTracer trac
252255
PluginClassLoaderRootPackageCustomizer.class,
253256
getAgentClassLoader());
254257
for (PluginClassLoaderRootPackageCustomizer rootPackageCustomizer : rootPackageCustomizers) {
255-
Collection<String> previous = pluginPackages2pluginClassLoaderRootPackages.put(
258+
PluginClassLoaderCustomizations customizations = new PluginClassLoaderCustomizations(
259+
rootPackageCustomizer.pluginClassLoaderRootPackages(),
260+
rootPackageCustomizer.requiredModuleOpens()
261+
);
262+
PluginClassLoaderCustomizations previous = pluginPackages2pluginClassLoaderCustomizations.put(
256263
rootPackageCustomizer.getPluginPackage(),
257-
Collections.unmodifiableList(new ArrayList<>(rootPackageCustomizer.pluginClassLoaderRootPackages())));
264+
customizations
265+
);
258266
if (previous != null) {
259267
throw new IllegalStateException("Only one PluginClassLoaderRootPackageCustomizer is allowed per plugin package: "
260268
+ rootPackageCustomizer.getPluginPackage());
@@ -316,6 +324,18 @@ public void run() {
316324
}
317325
}
318326

327+
public static boolean areModulesSupported() {
328+
return ModuleOpener.areModulesSupported();
329+
}
330+
331+
public static boolean openModule(Class<?> classFromTargetModule, ClassLoader openTo, Collection<String> packagesToOpen) {
332+
if (instrumentation == null) {
333+
throw new IllegalStateException("Can't open modules before the agent has been initialized");
334+
}
335+
return ModuleOpener.getInstance().openModuleTo(instrumentation, classFromTargetModule, openTo, packagesToOpen);
336+
}
337+
338+
319339
static synchronized void doReInitInstrumentation(Iterable<ElasticApmInstrumentation> instrumentations) {
320340
Logger logger = getLogger();
321341
logger.info("Re initializing instrumentation");
@@ -651,7 +671,7 @@ public static synchronized void reset() {
651671
instrumentation = null;
652672
IndyPluginClassLoaderFactory.clear();
653673
adviceClassName2instrumentationClassLoader.clear();
654-
pluginPackages2pluginClassLoaderRootPackages.clear();
674+
pluginPackages2pluginClassLoaderCustomizations.clear();
655675
}
656676

657677
private static AgentBuilder getAgentBuilder(final ByteBuddy byteBuddy, final CoreConfiguration coreConfiguration, final Logger logger,
@@ -922,10 +942,39 @@ public static ClassLoader getInstrumentationClassLoader(String adviceClass) {
922942
}
923943

924944
public static Collection<String> getPluginClassLoaderRootPackages(String pluginPackage) {
925-
Collection<String> pluginPackages = pluginPackages2pluginClassLoaderRootPackages.get(pluginPackage);
926-
if (pluginPackages != null) {
927-
return pluginPackages;
945+
PluginClassLoaderCustomizations customizations = pluginPackages2pluginClassLoaderCustomizations.get(pluginPackage);
946+
if (customizations != null) {
947+
return customizations.packages;
928948
}
929949
return Collections.singleton(pluginPackage);
930950
}
951+
952+
public static Map<String, List<String>> getRequiredPluginModuleOpens(String pluginPackage) {
953+
PluginClassLoaderCustomizations customizations = pluginPackages2pluginClassLoaderCustomizations.get(pluginPackage);
954+
if (customizations != null) {
955+
return customizations.requiredOpens;
956+
}
957+
return Collections.emptyMap();
958+
}
959+
960+
private static class PluginClassLoaderCustomizations {
961+
final List<String> packages;
962+
963+
final Map<String, List<String>> requiredOpens;
964+
965+
private PluginClassLoaderCustomizations(Collection<String> packages, Map<String, ? extends Collection<String>> requiredOpens) {
966+
this.packages = Collections.unmodifiableList(new ArrayList<>(packages));
967+
if (!requiredOpens.isEmpty()) {
968+
Map<String, List<String>> opens = new HashMap<>();
969+
for (Map.Entry<String, ? extends Collection<String>> open : requiredOpens.entrySet()) {
970+
if (!open.getValue().isEmpty()) {
971+
opens.put(open.getKey(), Collections.unmodifiableList(new ArrayList<>(open.getValue())));
972+
}
973+
}
974+
this.requiredOpens = Collections.unmodifiableMap(opens);
975+
} else {
976+
this.requiredOpens = Collections.emptyMap();
977+
}
978+
}
979+
}
931980
}

apm-agent-core/src/main/java/co/elastic/apm/agent/bci/IndyBootstrap.java

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import java.util.Collection;
5050
import java.util.Collections;
5151
import java.util.List;
52+
import java.util.Map;
5253
import java.util.concurrent.ConcurrentHashMap;
5354
import java.util.concurrent.ConcurrentMap;
5455

@@ -201,11 +202,6 @@ public class IndyBootstrap {
201202
*/
202203
public static final String LOOKUP_EXPOSER_CLASS_NAME = "co.elastic.apm.agent.bci.classloading.LookupExposer";
203204

204-
/**
205-
* The root package name prefix that all embedded plugins classes should start with
206-
*/
207-
private static final String EMBEDDED_PLUGINS_PACKAGE_PREFIX = "co.elastic.apm.agent.";
208-
209205
/**
210206
* Caches the names of classes that are defined within a package and it's subpackages
211207
*/
@@ -394,6 +390,7 @@ private static ConstantCallSite internalBootstrap(MethodHandles.Lookup lookup, S
394390
ClassLoader targetClassLoader = lookup.lookupClass().getClassLoader();
395391
ClassFileLocator classFileLocator;
396392
List<String> pluginClasses = new ArrayList<>();
393+
Map<String, List<String>> requiredModuleOpens = Collections.emptyMap();
397394
if (instrumentationClassLoader instanceof ExternalPluginClassLoader) {
398395
List<String> externalPluginClasses = ((ExternalPluginClassLoader) instrumentationClassLoader).getClassNames();
399396
for (String externalPluginClass : externalPluginClasses) {
@@ -413,7 +410,9 @@ private static ConstantCallSite internalBootstrap(MethodHandles.Lookup lookup, S
413410
ClassFileLocator.ForJarFile.of(agentJarFile)
414411
);
415412
} else {
416-
pluginClasses.addAll(getClassNamesFromBundledPlugin(adviceClassName, instrumentationClassLoader));
413+
String pluginPackage = PluginClassLoaderRootPackageCustomizer.getPluginPackageFromClassName(adviceClassName);
414+
pluginClasses.addAll(getClassNamesFromBundledPlugin(pluginPackage, instrumentationClassLoader));
415+
requiredModuleOpens = ElasticApmAgent.getRequiredPluginModuleOpens(pluginPackage);
417416
classFileLocator = ClassFileLocator.ForClassLoader.of(instrumentationClassLoader);
418417
}
419418
pluginClasses.add(LOOKUP_EXPOSER_CLASS_NAME);
@@ -428,6 +427,14 @@ private static ConstantCallSite internalBootstrap(MethodHandles.Lookup lookup, S
428427
// if config classes would be loaded from the plugin CL,
429428
// tracer.getConfig(Config.class) would return null when called from an advice as the classes are not the same
430429
.or(nameContains("Config").and(hasSuperType(is(ConfigurationOptionProvider.class)))));
430+
if (ElasticApmAgent.areModulesSupported() && !requiredModuleOpens.isEmpty()) {
431+
boolean success = addRequiredModuleOpens(requiredModuleOpens, targetClassLoader, pluginClassLoader);
432+
if (!success) {
433+
// must not be a static field as it would initialize logging before it's ready
434+
LoggerFactory.getLogger(IndyBootstrap.class).error("Cannot bootstrap advice because required modules could not be opened!");
435+
return null;
436+
}
437+
}
431438
Class<?> adviceInPluginCL = pluginClassLoader.loadClass(adviceClassName);
432439
Class<LookupExposer> lookupExposer = (Class<LookupExposer>) pluginClassLoader.loadClass(LOOKUP_EXPOSER_CLASS_NAME);
433440
// can't use MethodHandle.lookup(), see also https://github.com/elastic/apm-agent-java/issues/1450
@@ -444,11 +451,36 @@ private static ConstantCallSite internalBootstrap(MethodHandles.Lookup lookup, S
444451
}
445452
}
446453

447-
private static List<String> getClassNamesFromBundledPlugin(String adviceClassName, ClassLoader adviceClassLoader) throws IOException, URISyntaxException {
448-
if (!adviceClassName.startsWith(EMBEDDED_PLUGINS_PACKAGE_PREFIX)) {
449-
throw new IllegalArgumentException("invalid advice class location : " + adviceClassName);
454+
/**
455+
* Attempts to open required modules for a given plugin classloader.
456+
*
457+
* @param requiredModuleOpens A map of FQNs of "witness" classes from modules and the corresponding package names to be opened.
458+
* @param targetClassLoader the Classloader in which the class instrumented by the plugin classloader lives.
459+
* Will be used to lookup "witness" classes from modules.
460+
* @param pluginClassLoader the instrumentation plugin classloader which will be given module access.
461+
* @return true if all required modules could be opened. On java < 17, true may be returned
462+
* even if not all modules could be opened because in these versions module boundaries are not enforced.
463+
* If any of the provided witness classes is not found, false is always returned.
464+
*/
465+
private static boolean addRequiredModuleOpens(Map<String, List<String>> requiredModuleOpens, ClassLoader targetClassLoader, ClassLoader pluginClassLoader) {
466+
try {
467+
for (Map.Entry<String, List<String>> requiredOpen : requiredModuleOpens.entrySet()) {
468+
String witnessClassName = requiredOpen.getKey();
469+
List<String> packagesToOpen = requiredOpen.getValue();
470+
Class<?> witness = Class.forName(witnessClassName, false, targetClassLoader);
471+
if (!ElasticApmAgent.openModule(witness, pluginClassLoader, packagesToOpen)) {
472+
return false;
473+
}
474+
}
475+
} catch (ClassNotFoundException e) {
476+
// must not be a static field as it would initialize logging before it's ready
477+
LoggerFactory.getLogger(IndyBootstrap.class).error("Cannot open module because witness class is not found", e);
478+
return false;
450479
}
451-
String pluginPackage = adviceClassName.substring(0, adviceClassName.indexOf('.', EMBEDDED_PLUGINS_PACKAGE_PREFIX.length()));
480+
return true;
481+
}
482+
483+
private static List<String> getClassNamesFromBundledPlugin(String pluginPackage, ClassLoader adviceClassLoader) throws IOException, URISyntaxException {
452484
List<String> pluginClasses = classesByPackage.get(pluginPackage);
453485
if (pluginClasses == null) {
454486
pluginClasses = new ArrayList<>();
@@ -461,5 +493,4 @@ private static List<String> getClassNamesFromBundledPlugin(String adviceClassNam
461493
}
462494
return pluginClasses;
463495
}
464-
465496
}

apm-agent-core/src/main/java/co/elastic/apm/agent/bci/PluginClassLoaderRootPackageCustomizer.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@
1818
*/
1919
package co.elastic.apm.agent.bci;
2020

21+
import java.lang.instrument.Instrumentation;
2122
import java.util.Collection;
23+
import java.util.Collections;
24+
import java.util.Map;
25+
import java.util.Set;
2226

2327
/**
2428
* This class must be provided at most once per {@linkplain #getPluginPackage() plugin package}.
@@ -34,10 +38,14 @@ public abstract class PluginClassLoaderRootPackageCustomizer {
3438

3539
public PluginClassLoaderRootPackageCustomizer() {
3640
String className = getClass().getName();
41+
pluginPackage = getPluginPackageFromClassName(className);
42+
}
43+
44+
public static String getPluginPackageFromClassName(String className) {
3745
if (!className.startsWith(EMBEDDED_PLUGINS_PACKAGE_PREFIX)) {
3846
throw new IllegalArgumentException("invalid instrumentation class location : " + className);
3947
}
40-
this.pluginPackage = className.substring(0, className.indexOf('.', EMBEDDED_PLUGINS_PACKAGE_PREFIX.length()));
48+
return className.substring(0, className.indexOf('.', EMBEDDED_PLUGINS_PACKAGE_PREFIX.length()));
4149
}
4250

4351
public final String getPluginPackage() {
@@ -51,4 +59,34 @@ public final String getPluginPackage() {
5159
* If the {@linkplain #getPluginPackage() plugin package} should be part of the root packages, implementations need to explicitly add it.
5260
*/
5361
public abstract Collection<String> pluginClassLoaderRootPackages();
62+
63+
/**
64+
* Starting with Java 9, Java added a module system which allows restricting access to code within modules.
65+
* From Java 9 to 16, illegal access print a warning on a console.
66+
* Starting with Java 17, Illegal access cause {@link IllegalAccessException}s.
67+
* <p>
68+
* Instrumentation plugins are loaded in an isolated classloader and therefore in an unnamed module.
69+
* This module by default cannot access anything "private" within other modules, including
70+
* the module containing the instrumented class. If such an access is required for the plugin,
71+
* access can be granted using this method. Before anything from the plugin is invoked,
72+
* {@link Instrumentation#redefineModule(Module, Set, Map, Map, Set, Map) will be used to give the classloader
73+
* full access ("open") to the target module.}
74+
* <p>
75+
* <p>
76+
* Each entry within the result map corresponds to a module which needs to be made accessible.
77+
* Each module is looked up based on a "witness" class which resides within the respective module.
78+
* Therefore, the keys of the return value from this map are the fully qualified names of these witness classes.
79+
* The values of the map is a set of package names to open for the instrumentation plugin (like the
80+
* packages passed to the --add-opens command line argument).
81+
* <p>
82+
* IMPORTANT: The "witness" classes are looked up when an advice from the plugin is invoked for the first time.
83+
* It is best to provide classes which are known to be already loaded at that point in time in order
84+
* to not mess with class initialization order. For example, the instrumented class which invoked the
85+
* advice would be a safe choice.
86+
*
87+
* @return the map of "witness" FQNs to a collection of package names to open
88+
*/
89+
public Map<String, ? extends Collection<String>> requiredModuleOpens() {
90+
return Collections.emptyMap();
91+
}
5492
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package co.elastic.apm.agent.bci.modules;
20+
21+
import co.elastic.apm.agent.common.JvmRuntimeInfo;
22+
import co.elastic.apm.agent.sdk.logging.Logger;
23+
import co.elastic.apm.agent.sdk.logging.LoggerFactory;
24+
import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
25+
26+
import javax.annotation.Nullable;
27+
import java.lang.instrument.Instrumentation;
28+
import java.util.Collection;
29+
30+
@IgnoreJRERequirement
31+
public abstract class ModuleOpener {
32+
33+
private static final Logger logger = LoggerFactory.getLogger(ModuleOpener.class);
34+
@Nullable
35+
private static ModuleOpener instance;
36+
37+
private static final String IMPL_NAME = "co.elastic.apm.agent.bci.modules.ModuleOpenerImpl";
38+
39+
public static boolean areModulesSupported() {
40+
return JvmRuntimeInfo.ofCurrentVM().getMajorVersion() >= 9;
41+
}
42+
43+
public abstract boolean openModuleTo(Instrumentation instrumentation, Class<?> classFromTargetModule, ClassLoader openTo, Collection<String> packagesToOpen);
44+
45+
public static ModuleOpener getInstance() {
46+
if (instance == null) {
47+
synchronized (ModuleOpener.class) {
48+
if (instance == null) {
49+
if (areModulesSupported()) {
50+
try {
51+
instance = (ModuleOpener) Class.forName(IMPL_NAME).getDeclaredConstructor().newInstance();
52+
} catch (Exception e) {
53+
logger.error("Failed to initialize ModuleOpener", e);
54+
instance = new NoOp();
55+
}
56+
} else {
57+
instance = new NoOp();
58+
}
59+
}
60+
}
61+
}
62+
return instance;
63+
}
64+
65+
private static class NoOp extends ModuleOpener {
66+
67+
@Override
68+
public boolean openModuleTo(Instrumentation instrumentation, Class<?> classFromTargetModule, ClassLoader openTo, Collection<String> packagesToOpen) {
69+
return true;
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)