Support injecting resources into classloader and use it in aws-sdk-2.… (#1172)

* 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
This commit is contained in:
Anuraag Agrawal 2020-09-15 17:38:20 +09:00 committed by GitHub
parent fd07525744
commit a9f0e21bfe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 284 additions and 162 deletions

View File

@ -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<ClassLoader> 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<TypeDescription> typeMatcher() {
return nameStartsWith("software.amazon.awssdk.")
.and(
implementsInterface(
named("software.amazon.awssdk.core.client.builder.SdkClientBuilder")));
public ElementMatcher<ClassLoader> 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<? super TypeDescription> 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<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
Map<ElementMatcher.Junction<MethodDescription>, 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();
}
}

View File

@ -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<ClassLoader> classLoaderMatcher() {
// Optimization for expensive typeMatcher.
return hasClassesNamed("software.amazon.awssdk.core.client.config.ClientOverrideConfiguration");
}
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("software.amazon.awssdk.core.client.config.ClientOverrideConfiguration");
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, 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);
}
}
}

View File

@ -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<SdkClientBuilder, Boolean> OVERRIDDEN = newWeakMap();
public static class ScopeHolder {
public static final ThreadLocal<Scope> 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<ClientOverrideConfiguration.Builder>
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() {

View File

@ -0,0 +1 @@
io.opentelemetry.instrumentation.auto.awssdk.v2_2.TracingExecutionInterceptor

View File

@ -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

View File

@ -1 +1,7 @@
apply from: "$rootDir/gradle/instrumentation.gradle"
dependencies {
compileOnly project(':javaagent-bootstrap')
testImplementation project(':javaagent-bootstrap')
}

View File

@ -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.
*
* <p>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<? super TypeDescription> typeMatcher() {
return extendsClass(named("java.lang.ClassLoader"));
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, 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<URL> resources) {
URL helper = HelperResources.load(classLoader, name);
if (helper == null) {
return;
}
if (!resources.hasMoreElements()) {
resources = Collections.enumeration(Collections.singleton(helper));
return;
}
List<URL> 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);
}
}
}

View File

@ -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<URLClassLoader> 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()
}
}

View File

@ -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<ClassLoader, Map<String, URL>> 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<String, URL>());
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<String, URL> map = RESOURCES.get(classLoader);
if (map == null) {
return null;
}
return map.get(path);
}
private HelperResources() {}
}

View File

@ -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<String> helperClassNames;
private final Set<String> helperResourceNames;
private final Map<String, byte[]> dynamicTypeMap = new LinkedHashMap<>();
private final WeakMap<ClassLoader, Boolean> 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<String> helperClassNames, List<String> 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<String, byte[]> 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;
}

View File

@ -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<AnnotationSource> 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 */

View File

@ -115,6 +115,10 @@ public class GlobalIgnoresMatcher<T extends TypeDescription>
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

View File

@ -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<URLClassLoader> 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: