Skip to content
Closed
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,32 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

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

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time. <br>
* Explicitly defines the classloader that needs to be used to load classes.
*
* <ul>
* <li>when using inline instrumentation, this is ignored and behavior is equivalent to {@link
* ClassLoadingTarget#INSTRUMENTATION_TARGET} as classes are injected into the instrumented CL
* <li>when using indy instrumentation and not explicitly set, the behavior is equivalent to an
* explicit {@link ClassLoadingTarget#INSTRUMENTATION_ISOLATED}
* <li>when using indy and classes/packages need to be shared across multiple isolated
* classloaders, the {@link ClassLoadingTarget#INSTRUMENTATION_SHARED} should be used
* </ul>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.PACKAGE})
public @interface ClassLoadingStrategy {

ClassLoadingTarget value() default ClassLoadingTarget.INSTRUMENTATION_ISOLATED;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

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

/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time. <br>
*/
public enum ClassLoadingTarget {
/**
* Class or package will be injected into the instrumented classloader, as if the class was
* present in the application classpath. When referenced directly from instrumentation advice or
* helper classes, it should never be loaded in the instrumentation module or agent classloader.
*/
INSTRUMENTATION_TARGET,
/**
* Class or package will be injected into an isolated instrumentation module classloader when
* using InvokeDynamic instrumentation. This is the default for most instrumentation classes as
* they need to be isolated from the instrumented application. <br>
* When using inlined instrumentation, this is equivalent to {@link #INSTRUMENTATION_TARGET}.
*/
INSTRUMENTATION_ISOLATED,
/**
* Class or package will be injected into a shared instrumentation module classloader when using
* InvokeDynamic instrumentation. This should be used for shared libraries classes and classes
* that are used across multiple instrumentation modules. <br>
* When using inlined instrumentation, this is equivalent to {@link #INSTRUMENTATION_TARGET}.
*/
INSTRUMENTATION_SHARED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

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

import io.opentelemetry.javaagent.extension.instrumentation.internal.ClassLoadingStrategy;
import io.opentelemetry.javaagent.extension.instrumentation.internal.ClassLoadingTarget;
import java.io.IOException;
import java.io.InputStream;
import javax.annotation.Nullable;
import net.bytebuddy.utility.StreamDrainer;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;

/** Utility to help with class-loading indy modules */
public class ClassLoadingTargetUtil {

private static final Type STRATEGY_ANNOTATION = Type.getType(ClassLoadingStrategy.class);
private static final Type TARGET_ENUM = Type.getType(ClassLoadingTarget.class);

private ClassLoadingTargetUtil() {}

/**
* Reads the class class-loading strategy from class (or package) bytecode
*
* @param bytecode class or package bytecode
* @return class loading strategy, defaults to {@link ClassLoadingTarget#INSTRUMENTATION_ISOLATED}
* if annotation is not present.
*/
@Nullable
private static ClassLoadingTarget getTarget(byte[] bytecode) {
ClassReader cr = new ClassReader(bytecode);
ClassNode classNode = new ClassNode();
cr.accept(classNode, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
if (classNode.visibleAnnotations != null) {
for (AnnotationNode annotation : classNode.visibleAnnotations) {
if (Type.getType(annotation.desc).equals(STRATEGY_ANNOTATION)) {
for (Object value : annotation.values) {
if (value instanceof String[]) {
String[] array = (String[]) value;
if (array.length == 2 && Type.getType(array[0]).equals(TARGET_ENUM)) {
return ClassLoadingTarget.valueOf(array[1]);
}
}
}
}
}
}
return null;
}

/**
* Get class target with fallback on package target.
*
* @param className class name
* @param classLoader class loader to load class/package bytecode
* @return class loading target, or {@literal null} of no annotation is present
*/
@Nullable
public static ClassLoadingTarget getClassTarget(String className, ClassLoader classLoader) {
String classPath = className.replace(".", "/") + ".class";
byte[] byteCode = getByteCode(classPath, classLoader);
if (byteCode == null) {
// class is not present in this CL
return null;
}
ClassLoadingTarget classTarget = getTarget(byteCode);
if (null != classTarget) {
return classTarget;
}
String packageName = className.substring(0, className.lastIndexOf('.'));
ClassLoadingTarget packageTarget = packageTarget(packageName, classLoader);
if (packageTarget != null) {
return packageTarget;
}
return null;
}

// package-private for testing
@Nullable
static ClassLoadingTarget packageTarget(String packageName, ClassLoader classLoader) {
String packagePath = packageName.replace(".", "/") + "/package-info.class";
byte[] byteCode = getByteCode(packagePath, classLoader);
return byteCode == null ? null : getTarget(byteCode);
}

// package-private for testing
@Nullable
static ClassLoadingTarget classTarget(String className, ClassLoader classLoader) {
String classPath = className.replace(".", "/") + ".class";
byte[] byteCode = getByteCode(classPath, classLoader);
if (byteCode == null) {
return null;
}
return getTarget(byteCode);
}

@Nullable
private static byte[] getByteCode(String resourcePath, ClassLoader classLoader) {
try (InputStream input = classLoader.getResourceAsStream(resourcePath)) {
if (input == null) {
return null;
}
return StreamDrainer.DEFAULT.drain(input);
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

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

import static org.assertj.core.api.Assertions.assertThat;

import io.opentelemetry.javaagent.extension.instrumentation.internal.ClassLoadingStrategy;
import io.opentelemetry.javaagent.extension.instrumentation.internal.ClassLoadingTarget;
import io.opentelemetry.javaagent.tooling.instrumentation.indy.dummies.targetcl.DummyInherit;
import io.opentelemetry.javaagent.tooling.instrumentation.indy.dummies.targetcl.DummyOverride;
import org.junit.jupiter.api.Test;

public class ClassLoadingTargetUtilTest {

@Test
void checkTarget() {
// not defined at class nor package level
testStrategy(AClass.class, null);

// explicitly set at class level
testExplicitAnnotation(BClass.class, ClassLoadingTarget.INSTRUMENTATION_ISOLATED);
testExplicitAnnotation(CClass.class, ClassLoadingTarget.INSTRUMENTATION_SHARED);
testExplicitAnnotation(DClass.class, ClassLoadingTarget.INSTRUMENTATION_TARGET);

// explicitly set at package level
String packageName = "io.opentelemetry.javaagent.tooling.instrumentation.indy.dummies.targetcl";
assertThat(ClassLoadingTargetUtil.packageTarget(packageName, getClass().getClassLoader()))
.isEqualTo(ClassLoadingTarget.INSTRUMENTATION_SHARED);

// package defined values inherited on class
assertThat(DummyInherit.class.getAnnotation(ClassLoadingStrategy.class))
.describedAs("no annotation is present on class")
.isNull();
assertThat(
ClassLoadingTargetUtil.getClassTarget(
DummyInherit.class.getName(), getClass().getClassLoader()))
.describedAs("package annotation is applied for class lookup")
.isEqualTo(ClassLoadingTarget.INSTRUMENTATION_SHARED);

// class lookup has priority over package when defined on both
testExplicitAnnotation(DummyOverride.class, ClassLoadingTarget.INSTRUMENTATION_ISOLATED);

// should defend against non-existing class
assertThat(
ClassLoadingTargetUtil.getClassTarget(
"this.class.does.not.Exists", getClass().getClassLoader()))
.isNull();
}

private void testStrategy(Class<?> type, ClassLoadingTarget expected) {
assertThat(ClassLoadingTargetUtil.getClassTarget(type.getName(), getClass().getClassLoader()))
.isEqualTo(expected);
}

private void testExplicitAnnotation(Class<?> type, ClassLoadingTarget expected) {
ClassLoadingStrategy annotation = type.getAnnotation(ClassLoadingStrategy.class);
assertThat(annotation).isNotNull();
assertThat(annotation.value()).isEqualTo(expected);
testStrategy(type, expected);
}

private static class AClass {}

@ClassLoadingStrategy(ClassLoadingTarget.INSTRUMENTATION_ISOLATED)
private static class BClass {}

@ClassLoadingStrategy(ClassLoadingTarget.INSTRUMENTATION_SHARED)
private static class CClass {}

@ClassLoadingStrategy(ClassLoadingTarget.INSTRUMENTATION_TARGET)
private static class DClass {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

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

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

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

import io.opentelemetry.javaagent.extension.instrumentation.internal.ClassLoadingStrategy;
import io.opentelemetry.javaagent.extension.instrumentation.internal.ClassLoadingTarget;

@ClassLoadingStrategy(ClassLoadingTarget.INSTRUMENTATION_ISOLATED)
public class DummyOverride {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@ClassLoadingStrategy(ClassLoadingTarget.INSTRUMENTATION_SHARED)
package io.opentelemetry.javaagent.tooling.instrumentation.indy.dummies.targetcl;

import io.opentelemetry.javaagent.extension.instrumentation.internal.ClassLoadingStrategy;
import io.opentelemetry.javaagent.extension.instrumentation.internal.ClassLoadingTarget;
Loading