From a9f0e21bfe023ffa13bfd299ed3b904a846338bd Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Tue, 15 Sep 2020 17:38:20 +0900 Subject: [PATCH] =?UTF-8?q?Support=20injecting=20resources=20into=20classl?= =?UTF-8?q?oader=20and=20use=20it=20in=20aws-sdk-2.=E2=80=A6=20(#1172)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Support injecting resources into classloader and use it in aws-sdk-2.2 instrumentation. * Use URL for duplication check instead of reading content and inject URLs directly instead of reading to byte array first. * Remove getResource --- .../awssdk/v2_2/AwsClientInstrumentation.java | 70 +++++--------- .../AwsClientOverrideInstrumentation.java | 67 ------------- .../v2_2/TracingExecutionInterceptor.java | 41 +------- .../global/handlers/execution.interceptors | 1 + .../awssdk/v2_2/AbstractAws2ClientTest.groovy | 2 + .../java-classloader/java-classloader.gradle | 6 ++ .../ResourceInjectionInstrumentation.java | 94 +++++++++++++++++++ .../test/groovy/ResourceInjectionTest.groovy | 59 ++++++++++++ .../test-resources/test-resource.txt | 1 + .../javaagent/bootstrap/HelperResources.java | 55 +++++++++++ .../javaagent/tooling/HelperInjector.java | 25 ++++- .../javaagent/tooling/Instrumenter.java | 17 +++- .../tooling/matcher/GlobalIgnoresMatcher.java | 4 + .../javaagent/test/HelperInjectionTest.groovy | 4 +- 14 files changed, 284 insertions(+), 162 deletions(-) delete mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/auto/src/main/java/io/opentelemetry/instrumentation/auto/awssdk/v2_2/AwsClientOverrideInstrumentation.java create mode 100644 instrumentation/aws-sdk/aws-sdk-2.2/auto/src/main/resources/software/amazon/awssdk/global/handlers/execution.interceptors create mode 100644 instrumentation/java-classloader/src/main/java/io/opentelemetry/instrumentation/auto/javaclassloader/ResourceInjectionInstrumentation.java create mode 100644 instrumentation/java-classloader/src/test/groovy/ResourceInjectionTest.groovy create mode 100644 instrumentation/java-classloader/src/test/resources/test-resources/test-resource.txt create mode 100644 javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/HelperResources.java diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/auto/src/main/java/io/opentelemetry/instrumentation/auto/awssdk/v2_2/AwsClientInstrumentation.java b/instrumentation/aws-sdk/aws-sdk-2.2/auto/src/main/java/io/opentelemetry/instrumentation/auto/awssdk/v2_2/AwsClientInstrumentation.java index 539fb086ca..7a2dd5ec9f 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/auto/src/main/java/io/opentelemetry/instrumentation/auto/awssdk/v2_2/AwsClientInstrumentation.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/auto/src/main/java/io/opentelemetry/instrumentation/auto/awssdk/v2_2/AwsClientInstrumentation.java @@ -17,72 +17,46 @@ package io.opentelemetry.instrumentation.auto.awssdk.v2_2; import static io.opentelemetry.javaagent.tooling.ClassLoaderMatcher.hasClassesNamed; -import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.implementsInterface; -import static net.bytebuddy.matcher.ElementMatchers.isMethod; -import static net.bytebuddy.matcher.ElementMatchers.isPublic; -import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; import static net.bytebuddy.matcher.ElementMatchers.named; import com.google.auto.service.AutoService; import io.opentelemetry.javaagent.tooling.Instrumenter; import java.util.Collections; -import java.util.HashMap; import java.util.Map; -import net.bytebuddy.asm.Advice; import net.bytebuddy.description.method.MethodDescription; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; -import software.amazon.awssdk.core.client.builder.SdkClientBuilder; -/** AWS SDK v2 instrumentation */ +/** + * Injects resource file with reference to our {@link TracingExecutionInterceptor} to allow SDK's + * service loading mechanism to pick it up. + */ @AutoService(Instrumenter.class) -public final class AwsClientInstrumentation extends AbstractAwsClientInstrumentation { - +public class AwsClientInstrumentation extends AbstractAwsClientInstrumentation { @Override - public ElementMatcher classLoaderMatcher() { - // Optimization for expensive typeMatcher. - return hasClassesNamed("software.amazon.awssdk.core.client.builder.SdkClientBuilder"); + public String[] helperResourceNames() { + return new String[] { + "software/amazon/awssdk/global/handlers/execution.interceptors", + }; } @Override - public ElementMatcher typeMatcher() { - return nameStartsWith("software.amazon.awssdk.") - .and( - implementsInterface( - named("software.amazon.awssdk.core.client.builder.SdkClientBuilder"))); + public ElementMatcher classLoaderMatcher() { + // We don't actually transform it but want to make sure we only apply the instrumentation when + // our key dependency is present. + return hasClassesNamed("software.amazon.awssdk.core.interceptor.ExecutionInterceptor"); + } + + @Override + public ElementMatcher typeMatcher() { + // We don't actually need to transform anything but need a class to match to make sure our + // helpers are injected. Pick an arbitrary class we happen to reference. + return named("software.amazon.awssdk.core.SdkRequest"); } @Override public Map, String> transformers() { - Map, String> transformers = new HashMap<>(); - - transformers.put( - isMethod().and(isPublic()).and(named("build")), - AwsClientInstrumentation.class.getName() + "$AwsSdkClientBuilderBuildAdvice"); - - transformers.put( - isMethod().and(isPublic()).and(named("overrideConfiguration")), - AwsClientInstrumentation.class.getName() - + "$AwsSdkClientBuilderOverrideConfigurationAdvice"); - - return Collections.unmodifiableMap(transformers); - } - - public static class AwsSdkClientBuilderOverrideConfigurationAdvice { - - @Advice.OnMethodEnter(suppress = Throwable.class) - public static void methodEnter(@Advice.This SdkClientBuilder thiz) { - TracingExecutionInterceptor.OVERRIDDEN.put(thiz, true); - } - } - - public static class AwsSdkClientBuilderBuildAdvice { - - @Advice.OnMethodEnter(suppress = Throwable.class) - public static void methodEnter(@Advice.This SdkClientBuilder thiz) { - if (!Boolean.TRUE.equals(TracingExecutionInterceptor.OVERRIDDEN.get(thiz))) { - TracingExecutionInterceptor.overrideConfiguration(thiz); - } - } + // Nothing to do, helpers are injected but no class transformation happens here. + return Collections.emptyMap(); } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/auto/src/main/java/io/opentelemetry/instrumentation/auto/awssdk/v2_2/AwsClientOverrideInstrumentation.java b/instrumentation/aws-sdk/aws-sdk-2.2/auto/src/main/java/io/opentelemetry/instrumentation/auto/awssdk/v2_2/AwsClientOverrideInstrumentation.java deleted file mode 100644 index f705ce73fd..0000000000 --- a/instrumentation/aws-sdk/aws-sdk-2.2/auto/src/main/java/io/opentelemetry/instrumentation/auto/awssdk/v2_2/AwsClientOverrideInstrumentation.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.opentelemetry.instrumentation.auto.awssdk.v2_2; - -import static io.opentelemetry.javaagent.tooling.ClassLoaderMatcher.hasClassesNamed; -import static net.bytebuddy.matcher.ElementMatchers.isMethod; -import static net.bytebuddy.matcher.ElementMatchers.isPublic; -import static net.bytebuddy.matcher.ElementMatchers.isStatic; -import static net.bytebuddy.matcher.ElementMatchers.named; - -import com.google.auto.service.AutoService; -import io.opentelemetry.javaagent.tooling.Instrumenter; -import java.util.Collections; -import java.util.Map; -import net.bytebuddy.asm.Advice; -import net.bytebuddy.description.method.MethodDescription; -import net.bytebuddy.description.type.TypeDescription; -import net.bytebuddy.matcher.ElementMatcher; -import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; - -/** - * Separate instrumentation to inject into user configuration overrides. Overrides aren't merged so - * we need to either inject into their override or create our own, but not both. - */ -@AutoService(Instrumenter.class) -public final class AwsClientOverrideInstrumentation extends AbstractAwsClientInstrumentation { - - @Override - public ElementMatcher classLoaderMatcher() { - // Optimization for expensive typeMatcher. - return hasClassesNamed("software.amazon.awssdk.core.client.config.ClientOverrideConfiguration"); - } - - @Override - public ElementMatcher typeMatcher() { - return named("software.amazon.awssdk.core.client.config.ClientOverrideConfiguration"); - } - - @Override - public Map, String> transformers() { - return Collections.singletonMap( - isMethod().and(isPublic()).and(isStatic()).and(named("builder")), - AwsClientOverrideInstrumentation.class.getName() + "$AwsSdkClientOverrideAdvice"); - } - - public static class AwsSdkClientOverrideAdvice { - - @Advice.OnMethodExit(suppress = Throwable.class) - public static void methodEnter(@Advice.Return ClientOverrideConfiguration.Builder builder) { - TracingExecutionInterceptor.overrideConfiguration(builder); - } - } -} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/auto/src/main/java/io/opentelemetry/instrumentation/auto/awssdk/v2_2/TracingExecutionInterceptor.java b/instrumentation/aws-sdk/aws-sdk-2.2/auto/src/main/java/io/opentelemetry/instrumentation/auto/awssdk/v2_2/TracingExecutionInterceptor.java index bd5b435840..6fbf1aa040 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/auto/src/main/java/io/opentelemetry/instrumentation/auto/awssdk/v2_2/TracingExecutionInterceptor.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/auto/src/main/java/io/opentelemetry/instrumentation/auto/awssdk/v2_2/TracingExecutionInterceptor.java @@ -16,24 +16,18 @@ package io.opentelemetry.instrumentation.auto.awssdk.v2_2; -import static io.opentelemetry.instrumentation.auto.api.WeakMap.Provider.newWeakMap; - import io.opentelemetry.context.ContextUtils; import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.decorator.ClientDecorator; -import io.opentelemetry.instrumentation.auto.api.WeakMap; import io.opentelemetry.instrumentation.awssdk.v2_2.AwsSdk; import io.opentelemetry.trace.Span; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.Optional; -import java.util.function.Consumer; import org.reactivestreams.Publisher; import software.amazon.awssdk.core.SdkRequest; import software.amazon.awssdk.core.SdkResponse; import software.amazon.awssdk.core.async.AsyncRequestBody; -import software.amazon.awssdk.core.client.builder.SdkClientBuilder; -import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; import software.amazon.awssdk.core.interceptor.Context.AfterExecution; import software.amazon.awssdk.core.interceptor.Context.AfterMarshalling; import software.amazon.awssdk.core.interceptor.Context.AfterTransmission; @@ -60,45 +54,14 @@ import software.amazon.awssdk.http.SdkHttpResponse; */ public class TracingExecutionInterceptor implements ExecutionInterceptor { - // Keeps track of SDK clients that have been overridden by the user and don't need to be - // overridden by us. - public static final WeakMap OVERRIDDEN = newWeakMap(); - public static class ScopeHolder { public static final ThreadLocal CURRENT = new ThreadLocal<>(); } - // Note: it looks like this lambda doesn't get generated as a separate class file so we do not - // need to inject helper for it. - private static final Consumer - OVERRIDE_CONFIGURATION_CONSUMER = - builder -> - builder.addExecutionInterceptor( - new TracingExecutionInterceptor(AwsSdk.newInterceptor())); - private final ExecutionInterceptor delegate; - private TracingExecutionInterceptor(ExecutionInterceptor delegate) { - this.delegate = delegate; - } - - /** - * We keep this method here because it references Java8 classes and we would like to avoid - * compiling this for instrumentation code that should load into Java7. - */ - public static void overrideConfiguration(SdkClientBuilder client) { - // We intercept calls to overrideConfiguration to make sure when a user overrides the - // configuration, we join their configuration. This means all we need to do is call the method - // here and we will intercept the builder and add our interceptor. - client.overrideConfiguration(builder -> {}); - } - - /** - * We keep this method here because it references Java8 classes and we would like to avoid - * compiling this for instrumentation code that should load into Java7. - */ - public static void overrideConfiguration(ClientOverrideConfiguration.Builder builder) { - OVERRIDE_CONFIGURATION_CONSUMER.accept(builder); + public TracingExecutionInterceptor() { + delegate = AwsSdk.newInterceptor(); } public static void muzzleCheck() { diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/auto/src/main/resources/software/amazon/awssdk/global/handlers/execution.interceptors b/instrumentation/aws-sdk/aws-sdk-2.2/auto/src/main/resources/software/amazon/awssdk/global/handlers/execution.interceptors new file mode 100644 index 0000000000..72c00668c3 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/auto/src/main/resources/software/amazon/awssdk/global/handlers/execution.interceptors @@ -0,0 +1 @@ +io.opentelemetry.instrumentation.auto.awssdk.v2_2.TracingExecutionInterceptor diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.groovy b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.groovy index 19476163fc..eb35b6288b 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.groovy +++ b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.groovy @@ -52,7 +52,9 @@ import software.amazon.awssdk.services.sqs.model.CreateQueueRequest import software.amazon.awssdk.services.sqs.model.SendMessageRequest import spock.lang.AutoCleanup import spock.lang.Shared +import spock.lang.Unroll +@Unroll abstract class AbstractAws2ClientTest extends InstrumentationSpecification { private static final StaticCredentialsProvider CREDENTIALS_PROVIDER = StaticCredentialsProvider diff --git a/instrumentation/java-classloader/java-classloader.gradle b/instrumentation/java-classloader/java-classloader.gradle index 80b3cc1b15..41a638a14a 100644 --- a/instrumentation/java-classloader/java-classloader.gradle +++ b/instrumentation/java-classloader/java-classloader.gradle @@ -1 +1,7 @@ apply from: "$rootDir/gradle/instrumentation.gradle" + +dependencies { + compileOnly project(':javaagent-bootstrap') + + testImplementation project(':javaagent-bootstrap') +} \ No newline at end of file diff --git a/instrumentation/java-classloader/src/main/java/io/opentelemetry/instrumentation/auto/javaclassloader/ResourceInjectionInstrumentation.java b/instrumentation/java-classloader/src/main/java/io/opentelemetry/instrumentation/auto/javaclassloader/ResourceInjectionInstrumentation.java new file mode 100644 index 0000000000..73036b46ba --- /dev/null +++ b/instrumentation/java-classloader/src/main/java/io/opentelemetry/instrumentation/auto/javaclassloader/ResourceInjectionInstrumentation.java @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.instrumentation.auto.javaclassloader; + +import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.extendsClass; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.bootstrap.HelperResources; +import io.opentelemetry.javaagent.tooling.Instrumenter; +import java.net.URL; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * Instruments {@link ClassLoader} to have calls to get resources intercepted and check our map of + * helper resources that is filled by instrumentation when they need helpers. + * + *

We currently only intercept {@link ClassLoader#getResources(String)} because this is the case + * we are currently always interested in, where it's used for service loading. + */ +@AutoService(Instrumenter.class) +public class ResourceInjectionInstrumentation extends Instrumenter.Default { + + public ResourceInjectionInstrumentation() { + super("class-loader"); + } + + @Override + public ElementMatcher typeMatcher() { + return extendsClass(named("java.lang.ClassLoader")); + } + + @Override + public Map, String> transformers() { + return Collections.singletonMap( + isMethod().and(named("getResources")).and(takesArguments(String.class)), + ResourceInjectionInstrumentation.class.getName() + "$GetResourcesAdvice"); + } + + public static class GetResourcesAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit( + @Advice.This ClassLoader classLoader, + @Advice.Argument(0) String name, + @Advice.Return(readOnly = false) Enumeration resources) { + URL helper = HelperResources.load(classLoader, name); + if (helper == null) { + return; + } + + if (!resources.hasMoreElements()) { + resources = Collections.enumeration(Collections.singleton(helper)); + return; + } + + List result = Collections.list(resources); + boolean duplicate = false; + for (URL loadedUrl : result) { + if (helper.sameFile(loadedUrl)) { + duplicate = true; + break; + } + } + if (!duplicate) { + result.add(helper); + } + + resources = Collections.enumeration(result); + } + } +} diff --git a/instrumentation/java-classloader/src/test/groovy/ResourceInjectionTest.groovy b/instrumentation/java-classloader/src/test/groovy/ResourceInjectionTest.groovy new file mode 100644 index 0000000000..ff9b81888b --- /dev/null +++ b/instrumentation/java-classloader/src/test/groovy/ResourceInjectionTest.groovy @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import static io.opentelemetry.auto.util.gc.GCUtils.awaitGC + +import io.opentelemetry.auto.test.AgentTestRunner +import io.opentelemetry.javaagent.tooling.HelperInjector +import java.lang.ref.WeakReference +import java.util.concurrent.atomic.AtomicReference + +class ResourceInjectionTest extends AgentTestRunner { + + def "resources injected to non-delegating classloader"() { + setup: + String resourceName = 'test-resources/test-resource.txt' + HelperInjector injector = new HelperInjector("test", [], [resourceName]) + AtomicReference emptyLoader = new AtomicReference<>(new URLClassLoader(new URL[0], (ClassLoader) null)) + + when: + def resourceUrls = emptyLoader.get().getResources(resourceName) + then: + !resourceUrls.hasMoreElements() + + when: + URLClassLoader notInjectedLoader = new URLClassLoader(new URL[0], (ClassLoader) null) + + injector.transform(null, null, emptyLoader.get(), null) + resourceUrls = emptyLoader.get().getResources(resourceName) + + then: + resourceUrls.hasMoreElements() + resourceUrls.nextElement().openStream().text.trim() == 'Hello world!' + + !notInjectedLoader.getResources(resourceName).hasMoreElements() + + when: "references to emptyLoader are gone" + emptyLoader.get().close() // cleanup + def ref = new WeakReference(emptyLoader.get()) + emptyLoader.set(null) + + awaitGC(ref) + + then: "HelperInjector doesn't prevent it from being collected" + null == ref.get() + } +} diff --git a/instrumentation/java-classloader/src/test/resources/test-resources/test-resource.txt b/instrumentation/java-classloader/src/test/resources/test-resources/test-resource.txt new file mode 100644 index 0000000000..cd0875583a --- /dev/null +++ b/instrumentation/java-classloader/src/test/resources/test-resources/test-resource.txt @@ -0,0 +1 @@ +Hello world! diff --git a/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/HelperResources.java b/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/HelperResources.java new file mode 100644 index 0000000000..23d92a6e0c --- /dev/null +++ b/javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/HelperResources.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.bootstrap; + +import static io.opentelemetry.instrumentation.auto.api.WeakMap.Provider.newWeakMap; + +import io.opentelemetry.instrumentation.auto.api.WeakMap; +import java.net.URL; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A holder of resources needed by instrumentation. We store them in the bootstrap classloader so + * instrumentation can store from the agent classloader and apps can retrieve from the app + * classloader. + */ +public final class HelperResources { + + private static final WeakMap> RESOURCES = newWeakMap(); + + /** Registers the {@code payload} to be available to instrumentation at {@code path}. */ + public static void register(ClassLoader classLoader, String path, URL url) { + RESOURCES.putIfAbsent(classLoader, new ConcurrentHashMap()); + RESOURCES.get(classLoader).put(path, url); + } + + /** + * Returns a {@link URL} that can be used to retrieve the content of the resource at {@code path}, + * or {@code null} if no resource could be found at {@code path}. + */ + public static URL load(ClassLoader classLoader, String path) { + Map map = RESOURCES.get(classLoader); + if (map == null) { + return null; + } + + return map.get(path); + } + + private HelperResources() {} +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/HelperInjector.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/HelperInjector.java index 5ebc9d4c7f..5722192aff 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/HelperInjector.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/HelperInjector.java @@ -20,12 +20,13 @@ import static io.opentelemetry.instrumentation.auto.api.WeakMap.Provider.newWeak import static io.opentelemetry.javaagent.tooling.ClassLoaderMatcher.BOOTSTRAP_CLASSLOADER; import io.opentelemetry.instrumentation.auto.api.WeakMap; +import io.opentelemetry.javaagent.bootstrap.HelperResources; import java.io.File; import java.io.IOException; import java.lang.ref.WeakReference; +import java.net.URL; import java.nio.file.Files; import java.security.SecureClassLoader; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -61,6 +62,7 @@ public class HelperInjector implements Transformer { private final String requestingName; private final Set helperClassNames; + private final Set helperResourceNames; private final Map dynamicTypeMap = new LinkedHashMap<>(); private final WeakMap injectedClassLoaders = newWeakMap(); @@ -75,10 +77,12 @@ public class HelperInjector implements Transformer { * order provided. This is important if there is interdependency between helper classes that * requires them to be injected in a specific order. */ - public HelperInjector(String requestingName, String... helperClassNames) { + public HelperInjector( + String requestingName, List helperClassNames, List helperResourceNames) { this.requestingName = requestingName; - this.helperClassNames = new LinkedHashSet<>(Arrays.asList(helperClassNames)); + this.helperClassNames = new LinkedHashSet<>(helperClassNames); + this.helperResourceNames = new LinkedHashSet<>(helperResourceNames); } public HelperInjector(String requestingName, Map helperMap) { @@ -86,6 +90,8 @@ public class HelperInjector implements Transformer { helperClassNames = helperMap.keySet(); dynamicTypeMap.putAll(helperMap); + + helperResourceNames = Collections.emptySet(); } public static HelperInjector forDynamicTypes( @@ -161,6 +167,19 @@ public class HelperInjector implements Transformer { ensureModuleCanReadHelperModules(module); } + + if (!helperResourceNames.isEmpty()) { + for (String resourceName : helperResourceNames) { + URL resource = Utils.getAgentClassLoader().getResource(resourceName); + if (resource == null) { + log.debug("Helper resource {} requested but not found.", resourceName); + continue; + } + + HelperResources.register(classLoader, resourceName, resource); + } + } + return builder; } diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/Instrumenter.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/Instrumenter.java index b43332cc7d..038cb7074f 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/Instrumenter.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/Instrumenter.java @@ -66,6 +66,8 @@ public interface Instrumenter { private static final Logger log = LoggerFactory.getLogger(Default.class); + private static final String[] EMPTY = new String[0]; + // Added here instead of AgentInstaller's ignores because it's relatively // expensive. https://github.com/DataDog/dd-trace-java/pull/1045 public static final Junction NOT_DECORATOR_MATCHER = @@ -123,10 +125,14 @@ public interface Instrumenter { private AgentBuilder.Identified.Extendable injectHelperClasses( AgentBuilder.Identified.Extendable agentBuilder) { String[] helperClassNames = helperClassNames(); - if (helperClassNames.length > 0) { + String[] helperResourceNames = helperResourceNames(); + if (helperClassNames.length > 0 || helperResourceNames.length > 0) { agentBuilder = agentBuilder.transform( - new HelperInjector(getClass().getSimpleName(), helperClassNames)); + new HelperInjector( + getClass().getSimpleName(), + Arrays.asList(helperClassNames), + Arrays.asList(helperResourceNames))); } return agentBuilder; } @@ -201,7 +207,12 @@ public interface Instrumenter { /** @return Class names of helpers to inject into the user's classloader */ public String[] helperClassNames() { - return new String[0]; + return EMPTY; + } + + /** @return Resource names to inject into the user's classloader */ + public String[] helperResourceNames() { + return EMPTY; } /** @return A type matcher used to match the classloader under transform */ diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/matcher/GlobalIgnoresMatcher.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/matcher/GlobalIgnoresMatcher.java index acb931d56a..2cd0cf9bc4 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/matcher/GlobalIgnoresMatcher.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/matcher/GlobalIgnoresMatcher.java @@ -115,6 +115,10 @@ public class GlobalIgnoresMatcher if (name.startsWith("java.rmi.") || name.startsWith("java.util.concurrent.")) { return false; } + if (name.equals("java.lang.ClassLoader")) { + return false; + } + // Concurrent instrumentation modifies the structure of // Cleaner class incompatibly with java9+ modules. // Working around until a long-term fix for modules can be diff --git a/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/test/HelperInjectionTest.groovy b/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/test/HelperInjectionTest.groovy index 496f805a07..4c87eeec4d 100644 --- a/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/test/HelperInjectionTest.groovy +++ b/javaagent-tooling/src/test/groovy/io/opentelemetry/javaagent/test/HelperInjectionTest.groovy @@ -38,7 +38,7 @@ class HelperInjectionTest extends AgentSpecification { def "helpers injected to non-delegating classloader"() { setup: String helperClassName = HelperInjectionTest.getPackage().getName() + '.HelperClass' - HelperInjector injector = new HelperInjector("test", helperClassName) + HelperInjector injector = new HelperInjector("test", [helperClassName], []) AtomicReference emptyLoader = new AtomicReference<>(new URLClassLoader(new URL[0], (ClassLoader) null)) when: @@ -70,7 +70,7 @@ class HelperInjectionTest extends AgentSpecification { ByteBuddyAgent.install() AgentInstaller.installBytebuddyAgent(ByteBuddyAgent.getInstrumentation()) String helperClassName = HelperInjectionTest.getPackage().getName() + '.HelperClass' - HelperInjector injector = new HelperInjector("test", helperClassName) + HelperInjector injector = new HelperInjector("test", [helperClassName], []) URLClassLoader bootstrapChild = new URLClassLoader(new URL[0], (ClassLoader) null) when: