Search code examples
javabyte-buddyopen-telemetryjavaagents

NoClassDefFoundError when trying to use opentelemetry Instrumenter in javaagent extension


I want to implement extension to manage methods tracing. I've implemented TypeInstrumentation class to around and utilite class for instrumenter() as described opentelemetry instrumentation module:

import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.instrumentation.api.annotation.support.async.AsyncOperationEndSupport;
import io.opentelemetry.instrumentation.api.instrumenter.util.ClassAndMethod;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.implementation.bytecode.assign.Assigner;
import net.bytebuddy.matcher.ElementMatcher;

import java.lang.reflect.Method;
import java.util.Set;

import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext;
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.namedOneOf;
import static org.telemetry.extensions.MethodSingletons.instrumenter;

public class MethodInstrumentation implements TypeInstrumentation {
    private final String className;
    private final Set<String> methodNames;

    public MethodInstrumentation(String className, Set<String> methodNames) {
        this.className = className;
        this.methodNames = methodNames;
    }

    @Override
    public ElementMatcher<ClassLoader> classLoaderOptimization() {
        return hasClassesNamed(className);
    }

    @Override
    public ElementMatcher<TypeDescription> typeMatcher() {
        return hasSuperType(named(className));
    }

    @Override
    public void transform(TypeTransformer transformer) {
        transformer.applyAdviceToMethod(
                namedOneOf(methodNames.toArray(new String[0])),
                OpenLMethodInstrumentation.class.getName() + "$MethodAdvice");
    }

    @SuppressWarnings("unused")
    public static class MethodAdvice {

        @Advice.OnMethodEnter(suppress = Throwable.class)
        public static void onEnter(
                @Advice.Origin("#t") Class<?> declaringClass,
                @Advice.Origin("#m") String methodName,
                @Advice.Local("otelMethod") ClassAndMethod classAndMethod,
                @Advice.Local("otelContext") Context context,
                @Advice.Local("otelScope") Scope scope) {
            Context parentContext = currentContext();
            classAndMethod = ClassAndMethod.create(declaringClass, methodName);
            try {
                context = instrumenter().start(parentContext, classAndMethod);
            } catch (Throwable e) {
                System.out.println("SOme error happened!");
                System.out.println(e.getClass());
                System.out.println(e.getMessage());
                throw e;
            }
            scope = context.makeCurrent();
        }

        @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
        public static void stopSpan(
                @Advice.Origin Method method,
                @Advice.Local("otelMethod") ClassAndMethod classAndMethod,
                @Advice.Local("otelContext") Context context,
                @Advice.Local("otelScope") Scope scope,
                @Advice.Return(typing = Assigner.Typing.DYNAMIC, readOnly = false) Object returnValue,
                @Advice.Thrown Throwable throwable) {

            scope.close();
            returnValue =
                    AsyncOperationEndSupport.create(instrumenter(), Void.class, method.getReturnType())
                            .asyncEnd(context, classAndMethod, returnValue, throwable);
        }
    }
}

MethodInstrumenter utility class with instrumenter:

import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesGetter;
import io.opentelemetry.instrumentation.api.instrumenter.code.CodeSpanNameExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.util.ClassAndMethod;


public final class MethodSingletons {
    public MethodSingletons() {

    }

    private static final String INSTRUMENTATION_NAME = "io.opentelemetry.methods";

    private static final Instrumenter<ClassAndMethod, Void> INSTRUMENTER;

    static {
        CodeAttributesGetter<ClassAndMethod> codeAttributesGetter =
                ClassAndMethod.codeAttributesGetter();

        INSTRUMENTER =
                Instrumenter.<ClassAndMethod, Void>builder(
                                GlobalOpenTelemetry.get(),
                                INSTRUMENTATION_NAME,
                                CodeSpanNameExtractor.create(codeAttributesGetter))
                        .addAttributesExtractor(CodeAttributesExtractor.create(codeAttributesGetter))
                        .buildInstrumenter(SpanKindExtractor.alwaysInternal());
    }

    public static Instrumenter<ClassAndMethod, Void> instrumenter() {
        return INSTRUMENTER;
    }

}

After I build this code I'm adding it as extension to javaagent configuration:

-javaagent:/Users/..../target/opentelemetry-javaagent.jar
-Dotel.javaagent.extensions=/Users/..../target/org.tracing.opentelemetry-1.0.0-SNAPSHOT.jar

And get NoClassDefFoundError in instrumenter().start line:

class java.lang.NoClassDefFoundError
Could not initialize class org....telemetry.extensions.MethodSingletons

What is wrong? How I can configure instrumentation in opentelemetry extension?

UPD: I found that seems NoClassDefFoundError is related to CodeAttributesExtractor class in MethodInstrumentation class

UPD2: initial InstrumentationModule class:

import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;

import java.util.List;
import java.util.Set;

@AutoService(InstrumentationModule.class)
public class MethodsInstrumentationModule extends InstrumentationModule {

    public MethodsInstrumentationModule() {
        super("some-methods");
    }

    @Override
    public List<TypeInstrumentation> typeInstrumentations() {
        return List.of(
                new TypeInstrumentation("org.some.package.Some1",
                        Set.of("someMethod"))
        );
    }

}

Solution

  • It is required to inject helper classes as @Rafael Winterhalter mentioned. I found out how to do it. It is required to redefine getAdditionalHelperClassNames() method in InstrumentationModule class, as described here. So I added the next method to MethodsInstrumentationModule class and all works fine:

    @Override
        public List<String> getAdditionalHelperClassNames() {
            return List.of(MethodSingletons.class.getName());
        }