Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add capability for invokedynamic InstrumentationModules to inject proxies #9565

Merged
merged 21 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
25a8763
Implemented invokedynamic proxy injection for InstrumentationModules
JonasKunz Sep 26, 2023
c3a6a13
Merge remote-tracking branch 'otel/main' into instrumentation-proxies
JonasKunz Sep 26, 2023
7a03962
Comments and spotless
JonasKunz Sep 27, 2023
9209cc6
Remove unnecessary subpackage
JonasKunz Sep 27, 2023
a54e730
Added missing javadoc
JonasKunz Sep 27, 2023
57da000
spotless fixes
JonasKunz Sep 27, 2023
1aa563e
Enable AWS SDK instrumentation for indy via proxy mechanism
JonasKunz Sep 27, 2023
d48563c
Moved injection to new ExtendedInstrumentationModule
JonasKunz Oct 2, 2023
43fc618
Javadoc review fixes
JonasKunz Oct 2, 2023
80dee2f
Merge remote-tracking branch 'otel/main' into instrumentation-proxies
JonasKunz Oct 2, 2023
5adf59e
Fix AWS tests for indy, enable for testing with indy flag
JonasKunz Oct 2, 2023
ac77780
Moved and renamed ExtendedInstrumentationModule
JonasKunz Oct 4, 2023
6b9f958
Added default method for generating proxy with same name
JonasKunz Oct 4, 2023
466ca1b
Revert AWS changes
JonasKunz Oct 4, 2023
1b3352b
Added proxy in log4j 2.17 instrumentation
JonasKunz Oct 4, 2023
85829bb
Spotless fixes
JonasKunz Oct 4, 2023
4e541ca
Merge remote-tracking branch 'otel/main' into instrumentation-proxies
JonasKunz Oct 13, 2023
0a540a2
Merge remote-tracking branch 'otel/main' into instrumentation-proxies
JonasKunz Oct 16, 2023
cbe27b3
Remove type pool caching
JonasKunz Oct 18, 2023
0a3b736
typo fix
JonasKunz Oct 18, 2023
a788f7c
Merge remote-tracking branch 'otel/HEAD' into instrumentation-proxies
JonasKunz Oct 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,6 @@ public boolean isHelperClass(String className) {
return className.startsWith("io.opentelemetry.contrib.awsxray.");
}

@Override
JonasKunz marked this conversation as resolved.
Show resolved Hide resolved
public boolean isIndyModule() {
return false;
}

@Override
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
// We don't actually transform it but want to make sure we only apply the instrumentation when
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,49 @@
import io.opentelemetry.javaagent.extension.instrumentation.HelperResourceBuilder;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.ClassInjector;
import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.ExtendedInstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.InjectionMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@AutoService(InstrumentationModule.class)
public class AwsSdkInstrumentationModule extends AbstractAwsSdkInstrumentationModule {
public class AwsSdkInstrumentationModule extends AbstractAwsSdkInstrumentationModule
implements ExtendedInstrumentationModule {
public AwsSdkInstrumentationModule() {
super("aws-sdk-2.2-core");
}

@Override
@SuppressWarnings("unchecked")
public List<String> getAdditionalHelperClassNames() {
if (isIndyModule()) {
// With the invokedynamic approach, the SnsInstrumentationModule and SqsInstrumentationModule
Copy link
Contributor

@laurit laurit Oct 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is entirely correct. The reason why there are multiple modules is that muzzle check is done per module. Having sqs and sns instrumentation allows running muzzle checks for these separately so that the main instrumentation module can still apply even if sqs and sns dependencies are missing. Why this works for you when you dump these into one module is because you have disabled the muzzle checks for the indy instrumentation modules, if you'd add back the muzzle check code then I suspect doing this would cause muzzle failures (or if it doesn't then runtime failures when previously there would have been a muzzle failure). I think that using aws instrumentation to test the proxy code was an unfortunate complication because the multimodule thing going on there complicates things. These 3 modules are currently interacting so that if the muzzle checks for the optional modules pass they'll inject the classes that the main module will dynamically discover them. With indy these 3 module are isolated from each other so the trick of dynamically discovering classes injected by another module won't work. The easiest way around this is probably to have one instrumentation class loader per application class loader.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or you could choose some other instrumentation besides aws for testing the proxy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 3 modules are currently interacting so that if the muzzle checks for the optional modules pass they'll inject the classes that the main module will dynamically discover them. With indy these 3 module are isolated from each other so the trick of dynamically discovering classes injected by another module won't work.

I see, thanks for explaining this!
My prior understanding was that the AWS instrumentation is simply about injecting the library instrumentation as is and using the SQS / SNS discovery built into that library instrumentation. However, for the agent instrumentation we also want to protect agains the case of an SQS / SNS version mismatch, correct?

The easiest way around this is probably to have one instrumentation class loader per application class loader.

Sounds good, but I'd propose that the default should be that each InstrumentationModule gets its own classloader. I would expect that in most cases, especially with external extensions, this isolation is a feature, not a bug. This allows extensions to freely use utility libraries (e.g. apache commons, guava) without having to fear collisions.

I'd propose for InstrumentationModules to optionally declare a "module group name". Modules with the same group name instrumenting the same app-classloader would get put into the same InstrumentationModuleClassLoader. What do you think about this approach?

If you agree, I'd shortly outline this as a TODO comment in the AWS module so that we don't forget about.

Or you could choose some other instrumentation besides aws for testing the proxy.

Agree, I'll revert the AWS changes and choose a different instrumentation to include in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, I'll revert the AWS changes and choose a different instrumentation to include in this PR.

So I had a look at both the apache-dubbo and azur instrumentation and noticed that their tests don't fail currently with testIndy even though they should? I tried messing with the instrumentations (e.g. changing the resource paths) and the tests still were green. I'm thinking that something must be wrong with the tests there?

I decided to use the log4j instrumentation in this PR, because that one actually fails without the proxying with indy enabled.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at least with dubbo the issue is that the javaagent instrumentation has implementation(project(":instrumentation:apache-dubbo-2.7:library-autoconfigure")) which leaks over into tests so the tests will pass even without the javaagent instrumentation because they just pick up the manual instrumentation library that gets automatically configured

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created a PR to fix dubbo tests, azure tests (ok I ran only azure-core-1.14) fail for me, perhaps you didn't notice that they have

  @Override
  public boolean isIndyModule() {
    return false;
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok I ran only azure-core-1.14

How did you run those, I feel like I'm missing something.

What I did was:

  • Remove the isIndyModule() override from the 1.14 AzureSdkInstrumentationModule
  • Run the 1.14 AzureSdkTest from the IDE with -PtestIndy=true and verified that the module is actually loaded as IndyModule -> the test still passes

Btw what is the correct way of running selected tests from the command line?
I tried ./gradlew :instrumentation:azure-core:azure-core-1.14:javaagent:test -PtestIndy=true -i but that seems to just pick up recent test result from some kind of cache, even if I execute a clean beforehand:

> Task :instrumentation:azure-core:azure-core-1.14:javaagent:test FROM-CACHE
Custom actions are attached to task ':instrumentation:azure-core:azure-core-1.14:javaagent:test'.
Build cache key for task ':instrumentation:azure-core:azure-core-1.14:javaagent:test' is fd9f198ac1e6354f6d8e4f302611bd17
Task ':instrumentation:azure-core:azure-core-1.14:javaagent:test' is not up-to-date because:
  Output property 'binaryResultsDirectory' file /Users/jonas/git/otel/opentelemetry-java-instrumentation/instrumentation/azure-core/azure-core-1.14/javaagent/build/test-results/test/binary has been removed.
  Output property 'binaryResultsDirectory' file /Users/jonas/git/otel/opentelemetry-java-instrumentation/instrumentation/azure-core/azure-core-1.14/javaagent/build/test-results/test/binary/output.bin has been removed.
  Output property 'binaryResultsDirectory' file /Users/jonas/git/otel/opentelemetry-java-instrumentation/instrumentation/azure-core/azure-core-1.14/javaagent/build/test-results/test/binary/output.bin.idx has been removed.
  Output property 'binaryResultsDirectory' file /Users/jonas/git/otel/opentelemetry-java-instrumentation/instrumentation/azure-core/azure-core-1.14/javaagent/build/test-results/test/binary/results.bin has been removed.
  Output property 'reports.enabledReports.html.outputLocation' file /Users/jonas/git/otel/opentelemetry-java-instrumentation/instrumentation/azure-core/azure-core-1.14/javaagent/build/reports/tests/test has been removed.
Loaded cache entry for task ':instrumentation:azure-core:azure-core-1.14:javaagent:test' with cache key fd9f198ac1e6354f6d8e4f302611bd17
Could not execute [report metric STATISTICS_COLLECT_METRICS_OVERHEAD]
Could not execute [report metric STATISTICS_COLLECT_METRICS_OVERHEAD]
Could not execute [report metric STATISTICS_COLLECT_METRICS_OVERHEAD]
Could not execute [report metric STATISTICS_COLLECT_METRICS_OVERHEAD]
Could not execute [report metric STATISTICS_COLLECT_METRICS_OVERHEAD]
Could not execute [report metric STATISTICS_COLLECT_METRICS_OVERHEAD]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From command line I think you have to at least do :instrumentation:azure-core:azure-core-1.14:javaagent:cleanTest before and then run :instrumentation:azure-core:azure-core-1.14:javaagent:test --no-build-cache
I retested and it still fails for me as expected when running from ide. I also removed the isIndyModule method from AzureSdkInstrumentationModule and added -PtestIndy=true to the run options in idea. I ran the test on main not this branch if it matters.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran the test on main not this branch if it matters.

Looks like we have a winner there, that was the difference to make them fail for me too.

I think this can be explained by InstrumentationModule.registerHelperResources being non-functional for indy-modules on main. In this PR that functionally has been added back.

So for this PR the azure tests should fail because the classes referenced by the injected resources are not supposed to be present in the instrumented classloader, but apparently they are.
I suspect that this then is a testing classpath issue similar to dubbo.

This shouldn't block us on this PR. I'll try to verify my hypothesis and try to open a PR doing the same thing for azure what you did for dubbo.

// are not required anymore, because we don't inject the helpers which reference potentially
// missing classes
// Instead, those are loaded by the InstrumentationModuleClassloader and LinkageErrors are
// caught just like when using those classes as library instrumentation
List<String> helpers = new ArrayList<>();
InstrumentationModule[] modules = {
new SnsInstrumentationModule(), new SqsInstrumentationModule()
};
for (InstrumentationModule include : modules) {
try {
List<String> moduleRefs =
(List<String>)
include.getClass().getDeclaredMethod("getMuzzleHelperClassNames").invoke(include);
helpers.addAll(moduleRefs);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
return helpers;
} else {
return Collections.emptyList();
}
}

/**
* Injects resource file with reference to our {@link TracingExecutionInterceptor} to allow SDK's
* service loading mechanism to pick it up.
Expand All @@ -26,6 +62,15 @@ public void registerHelperResources(HelperResourceBuilder helperResourceBuilder)
helperResourceBuilder.register("software/amazon/awssdk/global/handlers/execution.interceptors");
}

@Override
public void injectClasses(ClassInjector injector) {
injector
.proxyBuilder(
"io.opentelemetry.instrumentation.awssdk.v2_2.autoconfigure.TracingExecutionInterceptor",
"io.opentelemetry.instrumentation.awssdk.v2_2.autoconfigure.TracingExecutionInterceptor")
.inject(InjectionMode.CLASS_ONLY);
}

@Override
void doTransform(TypeTransformer transformer) {
// Nothing to transform, this type instrumentation is only used for injecting resources.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.extension.instrumentation.internal.injection;

/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
public interface ClassInjector {

/**
* Create a builder for a proxy class which will be injected into the instrumented {@link
* ClassLoader}. The generated proxy will delegate to the original class, which is loaded in a
* separate classloader.
*
* <p>This removes the need for the proxied class and its dependencies to be visible (just like
* Advices) to the instrumented ClassLoader.
*
* @param classToProxy the fully qualified name of the class for which a proxy will be generated
* @param newProxyName the fully qualified name to use for the generated proxy
* @return a builder for further customizing the proxy. {@link
* ProxyInjectionBuilder#inject(InjectionMode)} must be called to actually inject the proxy.
*/
ProxyInjectionBuilder proxyBuilder(String classToProxy, String newProxyName);
JonasKunz marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.extension.instrumentation.internal.injection;
JonasKunz marked this conversation as resolved.
Show resolved Hide resolved

import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;

/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
public interface ExtendedInstrumentationModule {
JonasKunz marked this conversation as resolved.
Show resolved Hide resolved

/**
* Only functional for Modules where {@link InstrumentationModule#isIndyModule()} returns {@code
* true}.
*
* <p>Normally, helper and advice classes are loaded in a child classloader of the instrumented
* classloader. This method allows to inject classes directly into the instrumented classloader
* instead.
*
* @param injector the builder for injecting classes
*/
default void injectClasses(ClassInjector injector) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.extension.instrumentation.internal.injection;

/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
public enum InjectionMode {
CLASS_ONLY
// TODO: implement the modes RESOURCE_ONLY and CLASS_AND_RESOURCE
// This will require a custom URL implementation for byte arrays, similar to how bytebuddy's
// ByteArrayClassLoader does it

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.extension.instrumentation.internal.injection;

/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
public interface ProxyInjectionBuilder {

void inject(InjectionMode mode);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@

import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.ExtendedInstrumentationModule;
import io.opentelemetry.javaagent.tooling.HelperInjector;
import io.opentelemetry.javaagent.tooling.TransformSafeLogger;
import io.opentelemetry.javaagent.tooling.Utils;
import io.opentelemetry.javaagent.tooling.bytebuddy.LoggingFailSafeMatcher;
import io.opentelemetry.javaagent.tooling.config.AgentConfig;
import io.opentelemetry.javaagent.tooling.field.VirtualFieldImplementationInstaller;
import io.opentelemetry.javaagent.tooling.field.VirtualFieldImplementationInstallerFactory;
import io.opentelemetry.javaagent.tooling.instrumentation.indy.ClassInjectorImpl;
import io.opentelemetry.javaagent.tooling.instrumentation.indy.IndyModuleRegistry;
import io.opentelemetry.javaagent.tooling.instrumentation.indy.IndyTypeTransformerImpl;
import io.opentelemetry.javaagent.tooling.instrumentation.indy.PatchByteCodeVersionTransformer;
Expand Down Expand Up @@ -78,8 +80,25 @@ private AgentBuilder installIndyModule(

IndyModuleRegistry.registerIndyModule(instrumentationModule);

HelperResourceBuilderImpl helperResourceBuilder = new HelperResourceBuilderImpl();
instrumentationModule.registerHelperResources(helperResourceBuilder);

ClassInjectorImpl injectedClassesCollector = new ClassInjectorImpl(instrumentationModule);
if (instrumentationModule instanceof ExtendedInstrumentationModule) {
((ExtendedInstrumentationModule) instrumentationModule)
.injectClasses(injectedClassesCollector);
}

AgentBuilder.Transformer helperInjector =
new HelperInjector(
instrumentationModule.instrumentationName(),
injectedClassesCollector.getClassesToInject(),
helperResourceBuilder.getResources(),
instrumentationModule.getClass().getClassLoader(),
instrumentation);

// TODO (Jonas): Adapt MuzzleMatcher to use the same type lookup strategy as the
// InstrumentationModuleClassLoader
// InstrumentationModuleClassLoader (see IndyModuleTypePool)
// MuzzleMatcher muzzleMatcher = new MuzzleMatcher(logger, instrumentationModule, config);
VirtualFieldImplementationInstaller contextProvider =
virtualFieldInstallerFactory.create(instrumentationModule);
Expand All @@ -88,7 +107,8 @@ private AgentBuilder installIndyModule(
for (TypeInstrumentation typeInstrumentation : instrumentationModule.typeInstrumentations()) {
AgentBuilder.Identified.Extendable extendableAgentBuilder =
setTypeMatcher(agentBuilder, instrumentationModule, typeInstrumentation)
.transform(new PatchByteCodeVersionTransformer());
.transform(new PatchByteCodeVersionTransformer())
.transform(helperInjector);

// TODO (Jonas): we are not calling
// contextProvider.rewriteVirtualFieldsCalls(extendableAgentBuilder) anymore
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.tooling.instrumentation.indy;

import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.ClassInjector;
import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.InjectionMode;
import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.ProxyInjectionBuilder;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.pool.TypePool;

public class ClassInjectorImpl implements ClassInjector {

private final InstrumentationModule instrumentationModule;

private final Map<String, Function<ClassLoader, byte[]>> classesToInject;

private final IndyProxyFactory proxyFactory;

public ClassInjectorImpl(InstrumentationModule module) {
instrumentationModule = module;
classesToInject = new HashMap<>();
proxyFactory = IndyBootstrap.getProxyFactory(module);
}

public Map<String, Function<ClassLoader, byte[]>> getClassesToInject() {
return classesToInject;
}

@Override
public ProxyInjectionBuilder proxyBuilder(String classToProxy, String newProxyName) {
return new ProxyBuilder(classToProxy, newProxyName);
}

private class ProxyBuilder implements ProxyInjectionBuilder {

private final String classToProxy;
private final String proxyClassName;

ProxyBuilder(String classToProxy, String proxyClassName) {
this.classToProxy = classToProxy;
this.proxyClassName = proxyClassName;
}

@Override
public void inject(InjectionMode mode) {
if (mode != InjectionMode.CLASS_ONLY) {
throw new UnsupportedOperationException("Not yet implemented");
}
classesToInject.put(
proxyClassName,
cl -> {
TypePool typePool = IndyModuleTypePool.get(cl, instrumentationModule);
TypeDescription proxiedType = typePool.describe(classToProxy).resolve();
return proxyFactory.generateProxy(proxiedType, proxyClassName).getBytes();
});
}
}
}
Loading
Loading