Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -0,0 +1,20 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.bootstrap.advice;

import java.lang.invoke.MethodHandles;
import java.util.function.Supplier;

/**
* Helper class that provides a MethodHandles.Lookup that allows defining classes in this package.
*/
public final class AdviceForwardLookupSupplier implements Supplier<MethodHandles.Lookup> {

@Override
public MethodHandles.Lookup get() {
return MethodHandles.lookup();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
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.ForwardIndyAdviceTransformer;
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 @@ -134,7 +135,7 @@ private AgentBuilder installIndyModule(
return helpers;
};

AgentBuilder.Transformer helperInjector =
HelperInjector helperInjector =
new HelperInjector(
instrumentationModule.instrumentationName(),
helperGenerator,
Expand All @@ -145,18 +146,24 @@ private AgentBuilder installIndyModule(
VirtualFieldImplementationInstaller contextProvider =
virtualFieldInstallerFactory.create(instrumentationModule);

boolean allowClassVersionChange =
config.getBoolean("otel.javaagent.experimental.allow-class-version-change", false);
Comment on lines +149 to +150
Copy link
Contributor Author

Choose a reason for hiding this comment

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

for now left a flag for switching back to changing the class version, if there is no interest in keeping it could remove it

Copy link
Member

Choose a reason for hiding this comment

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

I support fewer options 😄

AgentBuilder agentBuilder = parentAgentBuilder;
for (TypeInstrumentation typeInstrumentation : instrumentationModule.typeInstrumentations()) {
AgentBuilder.Identified.Extendable extendableAgentBuilder =
AgentBuilder.Identified.Extendable extendableAgentBuilder;
AgentBuilder.Identified.Narrowable narrowableAgentBuilder =
setTypeMatcher(agentBuilder, instrumentationModule, typeInstrumentation)
.and(muzzleMatcher)
.transform(new PatchByteCodeVersionTransformer());
.and(muzzleMatcher);
if (allowClassVersionChange) {
extendableAgentBuilder =
narrowableAgentBuilder.transform(new PatchByteCodeVersionTransformer());
} else {
extendableAgentBuilder =
narrowableAgentBuilder
.transform(ConstantAdjuster.instance())
.transform(new ForwardIndyAdviceTransformer(helperInjector));
}

// TODO (Jonas): we are not calling
// contextProvider.rewriteVirtualFieldsCalls(extendableAgentBuilder) anymore
// As a result the advices should store `VirtualFields` as static variables instead of having
// the lookup inline
// We need to update our documentation on that
extendableAgentBuilder =
IndyModuleRegistry.initializeModuleLoaderOnMatch(
instrumentationModule, extendableAgentBuilder);
Expand All @@ -166,7 +173,6 @@ private AgentBuilder installIndyModule(
new IndyTypeTransformerImpl(extendableAgentBuilder, instrumentationModule);
typeInstrumentation.transform(typeTransformer);
extendableAgentBuilder = typeTransformer.getAgentBuilder();
// TODO (Jonas): make instrumentation of bytecode older than 1.4 opt-in via a config option
extendableAgentBuilder = contextProvider.injectFields(extendableAgentBuilder);

agentBuilder = extendableAgentBuilder;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

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

import io.opentelemetry.javaagent.bootstrap.IndyBootstrapDispatcher;
import io.opentelemetry.javaagent.bootstrap.advice.AdviceForwardLookupSupplier;
import io.opentelemetry.javaagent.extension.instrumentation.internal.AsmApi;
import io.opentelemetry.javaagent.tooling.HelperInjector;
import java.security.ProtectionDomain;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import net.bytebuddy.ClassFileVersion;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.AsmVisitorWrapper;
import net.bytebuddy.description.field.FieldDescription;
import net.bytebuddy.description.field.FieldList;
import net.bytebuddy.description.method.MethodList;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.pool.TypePool;
import net.bytebuddy.utility.JavaModule;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Handle;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.GeneratorAdapter;

/**
* Replaces {@code INVOKEDYNAMIC} instructions used for invoking advice with {@code INVOKESTATIC}
* instructions in a helper class that contains the original {@code INVOKEDYNAMIC} instruction in
* classes that do not support {@code INVOKEDYNAMIC} (i.e. pre Java 7 class files).
*/
public class ForwardIndyAdviceTransformer implements AgentBuilder.Transformer {
private static final AtomicInteger counter = new AtomicInteger();
private static final String bootForwardClassPackage =
AdviceForwardLookupSupplier.class.getPackage().getName();

private final HelperInjector helperInjector;

public ForwardIndyAdviceTransformer(HelperInjector helperInjector) {
this.helperInjector = helperInjector;
}

private static boolean isAtLeastJava7(TypeDescription typeDescription) {
ClassFileVersion classFileVersion = typeDescription.getClassFileVersion();
return classFileVersion != null && classFileVersion.getJavaVersion() >= 7;
}

@Override
public DynamicType.Builder<?> transform(
DynamicType.Builder<?> builder,
TypeDescription typeDescription,
ClassLoader classLoader,
JavaModule javaModule,
ProtectionDomain protectionDomain) {

// java 7+ class files already support invokedynamic
if (isAtLeastJava7(typeDescription)) {
return builder;
}

return builder.visit(
new AsmVisitorWrapper.AbstractBase() {
@Override
public ClassVisitor wrap(
TypeDescription typeDescription,
ClassVisitor classVisitor,
Implementation.Context context,
TypePool typePool,
FieldList<FieldDescription.InDefinedShape> fieldList,
MethodList<?> methodList,
int writerFlags,
int readerFlags) {

return new ClassVisitor(AsmApi.VERSION, classVisitor) {
final Map<String, Supplier<byte[]>> injectedClasses = new HashMap<>();

@Override
public void visitEnd() {
super.visitEnd();

// inject helper classes that forward to the advice using invokedynamic
if (!injectedClasses.isEmpty()) {
helperInjector.injectHelperClasses(classLoader, injectedClasses);
}
}

@Override
public MethodVisitor visitMethod(
int access,
String name,
String descriptor,
String signature,
String[] exceptions) {
MethodVisitor mv =
super.visitMethod(access, name, descriptor, signature, exceptions);
return new MethodVisitor(api, mv) {
@Override
public void visitInvokeDynamicInsn(
String name,
String descriptor,
Handle bootstrapMethodHandle,
Object... bootstrapMethodArguments) {
if (Type.getInternalName(IndyBootstrapDispatcher.class)
.equals(bootstrapMethodHandle.getOwner())) {

String adviceClassName = (String) bootstrapMethodArguments[3];
String forwardClassDotName =
classLoader == null
? bootForwardClassPackage + ".Forward$$" + counter.incrementAndGet()
: adviceClassName + "$$Forward$$" + counter.incrementAndGet();
String forwardClassSlasName = forwardClassDotName.replace('.', '/');

Supplier<byte[]> forwardClassBytes =
generateForwardClass(
forwardClassSlasName,
name,
descriptor,
bootstrapMethodHandle,
bootstrapMethodArguments);
injectedClasses.put(forwardClassDotName, forwardClassBytes);

// replace invokedynamic with invokestatic to the generated forwarder class
// the forwarder class will contain the original invokedynamic instruction
super.visitMethodInsn(
Opcodes.INVOKESTATIC, forwardClassSlasName, name, descriptor, false);
return;
}

super.visitInvokeDynamicInsn(
name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments);
}
};
}
};
}
});
}

private static Supplier<byte[]> generateForwardClass(
String forwardClassSlasName,
String methodName,
String methodDescriptor,
Handle bootstrapMethodHandle,
Object[] bootstrapMethodArguments) {
return () -> {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cw.visit(
Opcodes.V1_8,
Opcodes.ACC_PUBLIC,
forwardClassSlasName,
null,
Type.getInternalName(Object.class),
null);
MethodVisitor mv =
cw.visitMethod(
Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, methodName, methodDescriptor, null, null);
GeneratorAdapter ga =
new GeneratorAdapter(
mv, Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, methodName, methodDescriptor);
ga.loadArgs();
mv.visitInvokeDynamicInsn(
methodName, methodDescriptor, bootstrapMethodHandle, bootstrapMethodArguments);
ga.returnValue();
mv.visitMaxs(0, 0);
mv.visitEnd();
cw.visitEnd();

return cw.toByteArray();
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@
class ClassLoaderMap {
private static final Cache<ClassLoader, WeakReference<Map<Object, Object>>> data = Cache.weak();
private static final Map<Object, Object> bootLoaderData = new ConcurrentHashMap<>();
private static final HelperInjector helperInjector =
HelperInjector.forDynamicTypes(
ClassLoaderMap.class.getSimpleName(), Collections.emptyList(), null);
static Injector defaultInjector =
(classLoader, className, bytes) -> {
HelperInjector.injectHelperClasses(
helperInjector.injectHelperClasses(
classLoader, Collections.singletonMap(className, () -> bytes));
return Class.forName(className, false, classLoader);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import io.opentelemetry.javaagent.bootstrap.HelperResources;
import io.opentelemetry.javaagent.bootstrap.InjectedClassHelper;
import io.opentelemetry.javaagent.bootstrap.InjectedClassHelper.HelperClassInfo;
import io.opentelemetry.javaagent.bootstrap.advice.AdviceForwardLookupSupplier;
import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldLookupSupplier;
import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.InjectionMode;
import io.opentelemetry.javaagent.instrumentation.executors.ExecutorLookupSupplier;
Expand Down Expand Up @@ -289,26 +290,30 @@ private void injectHelperClasses(
if (isBootClassLoader(classLoader)) {
injectBootstrapClassLoader(classnameToBytes);
}
} catch (Exception e) {
} catch (RuntimeException e) {
if (logger.isLoggable(SEVERE)) {
logger.log(
SEVERE,
"Error preparing helpers while processing {0} for {1}. Failed to inject helper classes into instance {2}",
new Object[] {typeDescription, requestingName, classLoader},
e);
}
throw new IllegalStateException(e);
throw e;
}
}

public static void injectHelperClasses(
public void injectHelperClasses(
ClassLoader classLoader, Map<String, Supplier<byte[]>> classNameToBytes) {
if (isBootClassLoader(classLoader)) {
throw new UnsupportedOperationException("boot loader not supported");
}
if (classNameToBytes.isEmpty()) {
return;
}
if (classLoader == null) {
if (instrumentation == null) {
throw new UnsupportedOperationException("boot loader not supported");
}
injectBootstrapClassLoader(classNameToBytes);
return;
}

Map<String, HelperClass> map =
helperClasses.computeIfAbsent(classLoader, (unused) -> new ConcurrentHashMap<>());
Expand Down Expand Up @@ -339,6 +344,7 @@ private static Map<String, byte[]> resolve(Map<String, Supplier<byte[]>> classes
// because all generated virtual field classes are in the same package we can use lookup to
// define them
addPackageLookup(new VirtualFieldLookupSupplier());
addPackageLookup(new AdviceForwardLookupSupplier());
}

private static void addPackageLookup(Supplier<MethodHandles.Lookup> supplier) {
Expand All @@ -350,7 +356,7 @@ private static ClassInjector getClassInjector(String packageName) {
return lookup != null ? ClassInjector.UsingLookup.of(lookup) : null;
}

private void injectBootstrapClassLoader(Map<String, Supplier<byte[]>> inject) throws IOException {
private void injectBootstrapClassLoader(Map<String, Supplier<byte[]>> inject) {
Map<String, byte[]> classnameToBytes = resolve(inject);
if (helperInjectorListener != null) {
helperInjectorListener.onInjection(classnameToBytes);
Expand Down Expand Up @@ -404,7 +410,7 @@ private void injectBootstrapClassLoader(Map<String, Supplier<byte[]>> inject) th
// a reference count -- but for now, starting simple.

// Failures to create a tempDir are propagated as IOException and handled by transform
if (!classnameToBytes.isEmpty()) {
if (!classnameToBytes.isEmpty() && instrumentation != null) {
File tempDir = createTempDir();
try {
ClassInjector.UsingInstrumentation.of(
Expand All @@ -417,8 +423,12 @@ private void injectBootstrapClassLoader(Map<String, Supplier<byte[]>> inject) th
}
}

private static File createTempDir() throws IOException {
return Files.createTempDirectory("opentelemetry-temp-jars").toFile();
private static File createTempDir() {
try {
return Files.createTempDirectory("opentelemetry-temp-jars").toFile();
} catch (IOException exception) {
throw new IllegalStateException("Failed to create temporary directory.", exception);
}
}

private static void deleteTempDir(File file) {
Expand Down
Loading