diff --git a/graalvm-native-image-demo/opentelemetry-agent-native/README.md b/graalvm-native-image-demo/opentelemetry-agent-native/README.md new file mode 100644 index 00000000..25f69ff0 --- /dev/null +++ b/graalvm-native-image-demo/opentelemetry-agent-native/README.md @@ -0,0 +1,73 @@ +# About +This project adds GraalVM native image support for [OpenTelemetry-java-instrumentation](https://github.com/open-telemetry/opentelemetry-java-instrumentation) 1.32.0. It serves as an input for native image building of OT agent-enabled applications. + +There are two things that the GraalVM [agent support PR](https://github.com/oracle/graal/pull/8077) can't automatically handle when supporting the agent instrumentation in native image: +1. The actions in agent `premain`. Java agent usually initializes the context and registers the class transformations in the premain phase. Native image needs the `premain` to do part of such works, e.g. initializations to activate the agent at runtime is necessary, but the actual bytecode transformations should be excluded. In this project, we provide a native version of OT agent premain via GraalVM's substitution mechanism. See [Target_io_opentelemetry_javaagent_OpenTelemetryAgent](src/main/java/com/alibaba/jvm/Target_io_opentelemetry_javaagent_OpenTelemetryAgent.java) for details. +2. The JDK class modifications. GraalVM can automatically compile transformed application classes into native image, but can't do the same work for transformed JDK classes. Because GraalVM itself is a Java application. Modified JDK classes shall influence GraalVM's building behaviors as well. And GraalVM has modified some JDK classes to fit the native image runtime, agent modifications could conflict with GraalVM modifications. Therefore, agent developers should provide the native image runtime version of JDK transformations. + +### Native Image Runtime `premain` Support +There are two approaches to define native image runtime `premain` actions. +1. Dectect the actual runtime environment and provide different actions accordingly. For example, the `java.vm.name` system property is "Substrate VM" for native image: + ```java + String vm = System.getProperty("java.vm.name"); + if ("Substrate VM".equals(vm)) { + // In native image + ... + } else { + // In JVM + ... + } + ``` +2. Define native image runtime substitutions for `premain` with GraalVM APIs. The substitution classes, methods and fields will replace the originals at native image build time and take effects at native image runtime. See [here](https://www.graalvm.org/sdk/javadoc/com/oracle/svm/core/annotate/TargetClass.html) for the complete introduction of GraalVM's substitution annotation system. In this project, [Target_io_opentelemetry_javaagent_OpenTelemetryAgent](src/main/java/com/alibaba/jvm/Target_io_opentelemetry_javaagent_OpenTelemetryAgent.java) is the substitution entry point for `premain` actions. + +## Enhance Classes with Advice Annotations +GraalVM's substitution system can handle the basic class enhancement requirements. But in practice, agents may instrument classes in more complicated means. Take OpenTelemetry java agent for example, it uses [Byte Buddy](https://github.com/raphw/byte-buddy) for complicated instrumentations such as exception interception, instrumenting certain methods for all subclasses of one class in batch, etc. + +To ease the native image adaption, Byte-Buddy-styled annotations are also provided in the agent support PR. We refer to them as advice annotations. +The basic rules are: + +- Class annotation `@Aspect` defines which classes will be enhanced. See the [source](https://github.com/oracle/graal/pull/8077/files#diff-2f2c248dd98c839fd85314a71d90f0d435773c3f40fc8a388ed4a9bb50440a6e) for details. +- Method annotation `@Advice.Before` and `@Advice.After` define the actual enhancement before and after the specified method. We refer these methods as advice methods. The advice methods must be **public static**. See the [source](https://github.com/oracle/graal/pull/8077/files#diff-50c767f452a495e5b7db040c23c74c2bb03b472cadd52674d5647280ada2fdcb) for details. + +As the the agent support PR is not merged into GraalVM master, these annotations are still not publicly available. The preview version can be obtained by compiling the PR. In this project, [graal-sdk.jar](libs/graal-sdk.jar) for JDK 17, and [nativeimage.jar](libs/nativeimage.jar) for JDK 21 are provided. + +### Exemptions of JDK Methods +Some JDK methods can be exempted from providing native image version, because they are entirely not supported in native image and won't get called anyway. +For example, in OpenTelemetry java agent, `java.lang.ClassLoader#defineClass` is instrumented to make sure its super class is loaded before the class is defined, See [io.opentelemetry.javaagent.instrumentation.internal.classloader.DefineClassInstrumentation](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/instrumentation/internal/internal-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/DefineClassInstrumentation.java) for details. But in native image, no class can be actually defined at build time, so there is no need to make such enhancement. + +Another example is that `java.lang.ClassLoader#loadClass` is instrumented to inject helper classes when loading certain classes. This is a specified transformation action, and no need to take in native image runtime, because the transformed results have been already captured by native-image-agent at [the interception stage](https://github.com/oracle/graal/pull/8077#:~:text=in%20three%20stages%3A-,Interception%20stage%3A,-native%2Dimage%2Dagent). + +Here are examples of how to use the advice annotations. +### Declare classes to be instrumented +@Aspect annotation has 3 methods to declare different kinds of class-matching strategies: +1. `subClassOf`: match all subclasses of the specified class. See [LoggerAspect.java](src/main/java/com/alibaba/jvm/LoggerAspect.java) for details. +2. `implementInterface`: match all implementation classes of the specified class. See [CallableAspect.java](src/main/java/com/alibaba/jvm/CallableAspect.java) for details. +3. `matchers`: Match the specified classes. It could be one or more classes. See [ExecutorsAspect.java](src/main/java/com/alibaba/jvm/ExecutorsAspect.java) for details. + +### Declare Advice Methods +There are two kinds of advice methods, before and after, executing before and after the target method is invoked. +By default, the target method is matched by the annotated method's name and parameters. + +The advice methods must **public static**. + +Here are examples of how to use advice method annotations: + +- Match multiple methods with the same parameters: all methods in [ExecutorsAspect](src/main/java/com/alibaba/jvm/ExecutorsAspect.java) +- Rewrite input parameter: `beforeExecute` in [ExecutorsAspect](src/main/java/com/alibaba/jvm/ExecutorsAspect.java) +- Restrict returning type: `exitSubmitRunnable` in [ExecutorsAspect](src/main/java/com/alibaba/jvm/ExecutorsAspect.java) +- Exception interception: `exitSubmitRunnable` in [ExecutorsAspect](src/main/java/com/alibaba/jvm/ExecutorsAspect.java) +- Pass returning value from before method to after method: `enterSubmitCallable` and `exitSubmitCallable` in [ExecutorsAspect](src/main/java/com/alibaba/jvm/ExecutorsAspect.java) +- Refer to `this` in advice methods: `beforeRun` in [RunnableAspect](src/main/java/com/alibaba/jvm/RunnableAspect.java) +- Refer to the field of target method's class: Not supported by annotations, need to use reflection. + +### Extra Reflection Registration +[The interception stage](https://github.com/oracle/graal/pull/8077#:~:text=in%20three%20stages%3A-,Interception%20stage%3A,-native%2Dimage%2Dagent) is supposed to capture reflections. But it misses some reflections in OpenTelemetry case. For example, the following code should use reflection at runtime. +``` java +VirtualField, PropagatedContext> virtualField = VirtualField.find(Callable.class, PropagatedContext.class); +``` +But the build system of OT can generate the reflection result to replace the `find` invocation at build time by Gradle plugin. So GraalVM's native-image-agent can't see the reflection execution, and no corresponding configuration is recorded, resulting native image run time errors. + +[OTFeature](src/main/java/com/alibaba/jvm/OTFeature.java) is added for this issue. It cooperates with GraalVM's feature mechanism to register necessary reflection data at native image build time. + +### Shaded Class Dependency +OT shaded some classes to avoid runtime conflict. So transformed classes dumped at [the interception stage](https://github.com/oracle/graal/pull/8077#:~:text=in%20three%20stages%3A-,Interception%20stage%3A,-native%2Dimage%2Dagent) have dependencies on shaded classes, not original classes. To keep dependency consistent, the **shaded** classes should be used when adapting JDK class enhancements to native image version. \ No newline at end of file diff --git a/graalvm-native-image-demo/opentelemetry-agent-native/libs/graal-sdk.jar b/graalvm-native-image-demo/opentelemetry-agent-native/libs/graal-sdk.jar new file mode 100644 index 00000000..b6cf21dc Binary files /dev/null and b/graalvm-native-image-demo/opentelemetry-agent-native/libs/graal-sdk.jar differ diff --git a/graalvm-native-image-demo/opentelemetry-agent-native/libs/nativeimage.jar b/graalvm-native-image-demo/opentelemetry-agent-native/libs/nativeimage.jar new file mode 100644 index 00000000..46ef5251 Binary files /dev/null and b/graalvm-native-image-demo/opentelemetry-agent-native/libs/nativeimage.jar differ diff --git a/graalvm-native-image-demo/opentelemetry-agent-native/pom.xml b/graalvm-native-image-demo/opentelemetry-agent-native/pom.xml new file mode 100644 index 00000000..cd1f9447 --- /dev/null +++ b/graalvm-native-image-demo/opentelemetry-agent-native/pom.xml @@ -0,0 +1,96 @@ + + + + 4.0.0 + + com.alibaba.jvm + opentelemetry-agent-native + jar + 1.0-SNAPSHOT + + UTF-8 + 1.32.0 + ${ot.version}-alpha + + + + + io.opentelemetry.javaagent + opentelemetry-javaagent + ${ot.version} + + + io.opentelemetry.javaagent + opentelemetry-javaagent-extension-api + ${ot.alpha.version} + + + io.opentelemetry.javaagent + opentelemetry-javaagent-tooling + ${ot.alpha.version} + + + io.opentelemetry + opentelemetry-sdk-extension-autoconfigure + ${ot.version} + + + io.opentelemetry.javaagent + opentelemetry-javaagent-inst-native-support + ${ot.version} + + + io.opentelemetry.instrumentation + opentelemetry-instrumentation-annotations-support + ${ot.alpha.version} + + + + + org.graalvm.sdk + nativeimage + + 24.1.0-instru-dev + system + ${basedir}/libs/nativeimage.jar + + + org.graalvm.sdk + collections + 23.1.2 + + + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.12.1 + + 17 + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + UTF-8 + + + + + diff --git a/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/CallableAspect.java b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/CallableAspect.java new file mode 100644 index 00000000..57972737 --- /dev/null +++ b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/CallableAspect.java @@ -0,0 +1,39 @@ +package com.alibaba.jvm; + +import com.oracle.svm.core.annotate.Advice; +import com.oracle.svm.core.annotate.Aspect; +import io.opentelemetry.javaagent.bootstrap.executors.PropagatedContext; +import io.opentelemetry.javaagent.bootstrap.executors.TaskAdviceHelper; +import io.opentelemetry.javaagent.shaded.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.shaded.io.opentelemetry.context.Scope; + +import java.util.concurrent.Callable; + +/** + * Match classes implement {@link Callable}. + * This is the native image version of {@link io.opentelemetry.javaagent.instrumentation.executors.CallableInstrumentation}. + */ +@Aspect(implementInterface = "java.util.concurrent.Callable", onlyWith = Aspect.JDKClassOnly.class) +public class CallableAspect { + @Advice.Before("call") + public static Scope beforeCall(@Advice.This Callable task) { + try { + VirtualField, PropagatedContext> virtualField = + VirtualField.find(Callable.class, PropagatedContext.class); + return TaskAdviceHelper.makePropagatedContextCurrent(virtualField, task); + } catch (Throwable t) { + return null; + } + } + + @Advice.After(value = "call", onThrowable = Throwable.class) + public static void afterCall(@Advice.BeforeResult Scope scope) { + try { + if (scope != null) { + scope.close(); + } + } catch (Throwable t) { + //do nothing + } + } +} diff --git a/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/ExecutorsAspect.java b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/ExecutorsAspect.java new file mode 100644 index 00000000..2cee1f77 --- /dev/null +++ b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/ExecutorsAspect.java @@ -0,0 +1,241 @@ +package com.alibaba.jvm; + +import com.oracle.svm.core.annotate.Advice; +import com.oracle.svm.core.annotate.Aspect; +import io.opentelemetry.javaagent.shaded.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.bootstrap.executors.ContextPropagatingRunnable; +import io.opentelemetry.javaagent.bootstrap.executors.ExecutorAdviceHelper; +import io.opentelemetry.javaagent.bootstrap.executors.PropagatedContext; +import io.opentelemetry.javaagent.shaded.io.opentelemetry.context.Context; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.ForkJoinTask; +import java.util.concurrent.Future; +import java.util.function.Predicate; + +/** + * This is the native image version of + * {@link io.opentelemetry.javaagent.instrumentation.executors.JavaExecutorInstrumentation} + * and + * {@link io.opentelemetry.javaagent.instrumentation.executors.ExecutorMatchers}. + */ +@SuppressWarnings("OtelPrivateConstructorForUtilityClass") +@Aspect(matchers = { + "java.util.concurrent.CompletableFuture$ThreadPerTaskExecutor", + "java.util.concurrent.Executors$DelegatedExecutorService", + "java.util.concurrent.Executors$FinalizableDelegatedExecutorService", + "java.util.concurrent.ForkJoinPool", + "java.util.concurrent.ScheduledThreadPoolExecutor", + "java.util.concurrent.ThreadPoolExecutor", + "java.util.concurrent.ThreadPerTaskExecutor" }) +public class ExecutorsAspect { + + @Advice.ResultWrapper + public static class ResultWrapper { + @Advice.BeforeResult + public PropagatedContext ret; + public Optional newTask; + } + + @Advice.Before(value = { "execute", "addTask" }, notFoundAction = Advice.NotFoundAction.ignore) + public static ResultWrapper beforeExecute(@Advice.Rewrite(field = "newTask") Runnable task) { + try { + ResultWrapper resultWrapper = new ResultWrapper(); + Context context = Java8BytecodeBridge.currentContext(); + if (!ExecutorAdviceHelper.shouldPropagateContext(context, task)) { + return resultWrapper; + } + if (ContextPropagatingRunnable.shouldDecorateRunnable(task)) { + resultWrapper.newTask = Optional + .of(ContextPropagatingRunnable.propagateContext(task, context)); + return resultWrapper; + } + VirtualField virtualField = VirtualField.find(Runnable.class, + PropagatedContext.class); + resultWrapper.ret = ExecutorAdviceHelper.attachContextToTask(context, virtualField, task); + return resultWrapper; + } catch (Throwable t) { + ResultWrapper resultWrapper = new ResultWrapper(); + resultWrapper.newTask = null; + resultWrapper.ret = null; + return resultWrapper; + } + } + + @Advice.After(value = { "execute", + "addTask" }, onThrowable = Throwable.class, notFoundAction = Advice.NotFoundAction.ignore) + public static void afterExecute( + @Advice.BeforeResult ResultWrapper resultWrapper, + @Advice.Thrown Throwable throwable) { + try { + ExecutorAdviceHelper.cleanUpAfterSubmit(resultWrapper.ret, throwable); + } catch (Throwable t) { + } + + } + + @Advice.Before(value = { "execute", "submit", + "invoke" }, notFoundAction = Advice.NotFoundAction.ignore) + public static PropagatedContext enterJobSubmit(ForkJoinTask task) { + try { + Context context = Java8BytecodeBridge.currentContext(); + if (ExecutorAdviceHelper.shouldPropagateContext(context, task)) { + VirtualField, PropagatedContext> virtualField = VirtualField.find(ForkJoinTask.class, + PropagatedContext.class); + return ExecutorAdviceHelper.attachContextToTask(context, virtualField, task); + } + return null; + } catch (Throwable t) { + return null; + } + } + + @Advice.After(value = { "execute", "submit", + "invoke" }, onThrowable = Throwable.class, notFoundAction = Advice.NotFoundAction.ignore) + public static void exitJobSubmit( + @Advice.BeforeResult PropagatedContext propagatedContext, + @Advice.Thrown Throwable throwable) { + try { + ExecutorAdviceHelper.cleanUpAfterSubmit(propagatedContext, throwable); + } catch (Throwable t) { + + } + } + + @Advice.Before(value = { "submit", + "schedule" }, notFoundAction = Advice.NotFoundAction.ignore, onlyWithReturnType = SubClassOfFuture.class) + public static PropagatedContext enterSubmitRunnable(Runnable task) { + try { + Context context = Java8BytecodeBridge.currentContext(); + if (ExecutorAdviceHelper.shouldPropagateContext(context, task)) { + VirtualField virtualField = VirtualField.find(Runnable.class, + PropagatedContext.class); + return ExecutorAdviceHelper.attachContextToTask(context, virtualField, task); + } + return null; + } catch (Throwable t) { + return null; + } + } + + @Advice.After(value = { "submit", + "schedule" }, onThrowable = Throwable.class, notFoundAction = Advice.NotFoundAction.ignore, onlyWithReturnType = SubClassOfFuture.class) + public static void exitSubmitRunnable(Runnable task, + @Advice.BeforeResult PropagatedContext propagatedContext, + @Advice.Thrown Throwable throwable, + @Advice.Return Future future) { + try { + if (propagatedContext != null && future != null) { + VirtualField, PropagatedContext> virtualField = VirtualField.find(Future.class, + PropagatedContext.class); + virtualField.set(future, propagatedContext); + } + ExecutorAdviceHelper.cleanUpAfterSubmit(propagatedContext, throwable); + } catch (Throwable t) { + + } + } + + @Advice.Before(value = { "submit", + "schedule" }, notFoundAction = Advice.NotFoundAction.ignore, onlyWithReturnType = SubClassOfFuture.class) + public static PropagatedContext enterSubmitCallable(Callable task) { + try { + Context context = Java8BytecodeBridge.currentContext(); + if (ExecutorAdviceHelper.shouldPropagateContext(context, task)) { + VirtualField, PropagatedContext> virtualField = VirtualField.find(Callable.class, + PropagatedContext.class); + return ExecutorAdviceHelper.attachContextToTask(context, virtualField, task); + } + return null; + } catch (Throwable t) { + return null; + } + } + + @Advice.After(value = { "submit", + "schedule" }, onThrowable = Throwable.class, notFoundAction = Advice.NotFoundAction.ignore, onlyWithReturnType = SubClassOfFuture.class) + public static void exitSubmitCallable( + Callable task, + @Advice.BeforeResult PropagatedContext propagatedContext, + @Advice.Thrown Throwable throwable, + @Advice.Return Future future) { + try { + if (propagatedContext != null && future != null) { + VirtualField, PropagatedContext> virtualField = VirtualField.find(Future.class, + PropagatedContext.class); + virtualField.set(future, propagatedContext); + } + ExecutorAdviceHelper.cleanUpAfterSubmit(propagatedContext, throwable); + } catch (Throwable t) { + + } + } + + @Advice.Before(value = { "invokeAny", "invokeAll" }, notFoundAction = Advice.NotFoundAction.ignore) + public static Collection submitEnter( + Collection> tasks) { + try { + if (tasks == null) { + return Collections.emptyList(); + } + + Context context = Java8BytecodeBridge.currentContext(); + for (Callable task : tasks) { + if (ExecutorAdviceHelper.shouldPropagateContext(context, task)) { + VirtualField, PropagatedContext> virtualField = VirtualField.find(Callable.class, + PropagatedContext.class); + ExecutorAdviceHelper.attachContextToTask(context, virtualField, task); + } + } + + // returning tasks and not propagatedContexts to avoid allocating another list + // just for an edge case (exception) + return tasks; + } catch (Throwable t) { + return null; + } + } + + @Advice.After(value = { "invokeAny", + "invokeAll" }, onThrowable = Throwable.class, notFoundAction = Advice.NotFoundAction.ignore) + public static void submitExit(Collection> originalTasks, + @Advice.BeforeResult Collection> tasks, + @Advice.Thrown Throwable throwable) { + try { + /* + * Note1: invokeAny doesn't return any futures so all we need to do for it + * is to make sure we close all scopes in case of an exception. + * Note2: invokeAll does return futures - but according to its documentation + * it actually only returns after all futures have been completed - i.e. it + * blocks. + * This means we do not need to setup any hooks on these futures, we just need + * to clear + * any parent spans in case of an error. + * (according to ExecutorService docs and AbstractExecutorService code) + */ + if (throwable != null) { + for (Callable task : tasks) { + if (task != null) { + VirtualField, PropagatedContext> virtualField = VirtualField.find(Callable.class, + PropagatedContext.class); + PropagatedContext propagatedContext = virtualField.get(task); + ExecutorAdviceHelper.cleanUpAfterSubmit(propagatedContext, throwable); + } + } + } + } catch (Throwable t) { + + } + } + + static class SubClassOfFuture implements Predicate> { + @Override + public boolean test(Class clazz) { + return Advice.isInterfaceOf(Future.class, clazz); + } + } +} diff --git a/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/ForkJoinTaskAspect.java b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/ForkJoinTaskAspect.java new file mode 100644 index 00000000..73a6d31f --- /dev/null +++ b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/ForkJoinTaskAspect.java @@ -0,0 +1,100 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.alibaba.jvm; + +import com.oracle.svm.core.annotate.Advice; +import com.oracle.svm.core.annotate.Aspect; +import io.opentelemetry.javaagent.shaded.io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.shaded.io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.shaded.instrumentation.api.util.VirtualField; + +import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.bootstrap.executors.ExecutorAdviceHelper; +import io.opentelemetry.javaagent.bootstrap.executors.PropagatedContext; +import io.opentelemetry.javaagent.bootstrap.executors.TaskAdviceHelper; + +import java.util.concurrent.Callable; +import java.util.concurrent.ForkJoinTask; + +/** + * Match subclasses of {@link ForkJoinTask} provided by JDK. + * This is the native image version of {@link io.opentelemetry.javaagent.instrumentation.executors.JavaForkJoinTaskInstrumentation}. + */ +@SuppressWarnings("OtelPrivateConstructorForUtilityClass") +@Aspect(subClassOf = "java.util.concurrent.ForkJoinTask", onlyWith = Aspect.JDKClassOnly.class) +public class ForkJoinTaskAspect { + @Advice.Before + public static Scope exec(@Advice.This ForkJoinTask thisRef) { + try { + VirtualField, PropagatedContext> virtualField = VirtualField.find(ForkJoinTask.class, + PropagatedContext.class); + Scope scope = TaskAdviceHelper.makePropagatedContextCurrent(virtualField, thisRef); + if (thisRef instanceof Runnable) { + VirtualField runnableVirtualField = VirtualField.find(Runnable.class, + PropagatedContext.class); + Scope newScope = TaskAdviceHelper.makePropagatedContextCurrent(runnableVirtualField, (Runnable) thisRef); + if (null != newScope) { + if (null != scope) { + newScope.close(); + } else { + scope = newScope; + } + } + } + if (thisRef instanceof Callable) { + VirtualField, PropagatedContext> callableVirtualField = VirtualField.find(Callable.class, + PropagatedContext.class); + Scope newScope = TaskAdviceHelper.makePropagatedContextCurrent( + callableVirtualField, (Callable) thisRef); + if (null != newScope) { + if (null != scope) { + newScope.close(); + } else { + scope = newScope; + } + } + } + return scope; + } catch (Throwable t) { + // suppress exception + return null; + } + } + + @Advice.After(value = "exec", onThrowable = Throwable.class) + public static void afterExec(@Advice.BeforeResult Scope scope) { + try { + if (scope != null) { + scope.close(); + } + } catch (Throwable t) { + + } + + } + + @Advice.Before("fork") + public static PropagatedContext beforeFork(@Advice.This ForkJoinTask task) { + try { + Context context = Java8BytecodeBridge.currentContext(); + if (ExecutorAdviceHelper.shouldPropagateContext(context, task)) { + VirtualField, PropagatedContext> virtualField = VirtualField.find(ForkJoinTask.class, + PropagatedContext.class); + return ExecutorAdviceHelper.attachContextToTask(context, virtualField, task); + } + return null; + } catch (Throwable t) { + return null; + } + } + + @Advice.After(value = "fork", onThrowable = Throwable.class) + public static void afterFork( + @Advice.BeforeResult PropagatedContext propagatedContext, + @Advice.Thrown Throwable throwable) { + ExecutorAdviceHelper.cleanUpAfterSubmit(propagatedContext, throwable); + } +} diff --git a/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/FutureAspect.java b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/FutureAspect.java new file mode 100644 index 00000000..c0b8a7c2 --- /dev/null +++ b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/FutureAspect.java @@ -0,0 +1,53 @@ +package com.alibaba.jvm; + +import com.oracle.svm.core.annotate.Advice; +import com.oracle.svm.core.annotate.Aspect; +import io.opentelemetry.javaagent.bootstrap.executors.ExecutorAdviceHelper; +import io.opentelemetry.javaagent.bootstrap.executors.PropagatedContext; +import io.opentelemetry.javaagent.shaded.instrumentation.api.util.VirtualField; + +import java.util.concurrent.Future; +import java.util.function.Predicate; + +/** + * This is the native image version of + * {@link io.opentelemetry.javaagent.instrumentation.executors.FutureInstrumentation}. + */ +@Aspect(matchers = { + // "java.util.concurrent.CompletableFuture$BiApply", + // "java.util.concurrent.CompletableFuture$BiCompletion", + "java.util.concurrent.CompletableFuture$BiRelay", + "java.util.concurrent.CompletableFuture$ThreadPerTaskExecutor", + // "java.util.concurrent.CountedCompleter", // JDK17 no cancel(boolean) method + // in this class + // "java.util.concurrent.ExecutorCompletionService$QueueingFuture", + "java.util.concurrent.ForkJoinTask", + "java.util.concurrent.ForkJoinTask$AdaptedCallable", + // "java.util.concurrent.ForkJoinTask$RunnableExecuteAction", + "java.util.concurrent.FutureTask", + // "java.util.concurrent.RecursiveAction", // JDK17 no cancel(boolean) method in + // this class + "java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask", +}) +public class FutureAspect { + + @Advice.After(onlyWithReturnType = IsBoolean.class, notFoundAction = Advice.NotFoundAction.info) + public static void cancel(boolean mayInterruptIfRunning, @Advice.This Future future) { + try { + VirtualField, PropagatedContext> virtualField = VirtualField.find(Future.class, + PropagatedContext.class); + ExecutorAdviceHelper.cleanPropagatedContext(virtualField, future); + } catch (Throwable t) { + + } + + } + + static class IsBoolean implements Predicate> { + + @Override + public boolean test(Class clazz) { + return clazz.equals(boolean.class); + } + } +} diff --git a/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/LoggerAspect.java b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/LoggerAspect.java new file mode 100644 index 00000000..0a1f5d8e --- /dev/null +++ b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/LoggerAspect.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.alibaba.jvm; + +import com.oracle.svm.core.annotate.Advice; +import com.oracle.svm.core.annotate.Aspect; +import com.oracle.svm.core.annotate.Aspect.JDKClassOnly; + +import io.opentelemetry.javaagent.bootstrap.CallDepth; +import io.opentelemetry.javaagent.instrumentation.jul.JavaUtilLoggingHelper; +import io.opentelemetry.javaagent.shaded.io.opentelemetry.api.logs.LoggerProvider; + +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +/** + * Match all subclasses of java.util.logging.Logger. + * This is the native image version of {@link io.opentelemetry.javaagent.instrumentation.jul.JavaUtilLoggingInstrumentationModule}. + */ +@Aspect(subClassOf = "java.util.logging.Logger", onlyWith = JDKClassOnly.class) +final public class LoggerAspect { + @SuppressWarnings("EmptyCatch") + @Advice.Before("log") + public static CallDepth beforeLog(LogRecord logRecord, @Advice.This Logger logger) { + CallDepth otelCallDepth = null; + try { + otelCallDepth = CallDepth.forClass(LoggerProvider.class); + if (otelCallDepth.getAndIncrement() == 0) { + JavaUtilLoggingHelper.capture(logger, logRecord); + } + } catch (Throwable throwable) { + + } + return otelCallDepth; + } + + @SuppressWarnings("EmptyCatch") + @Advice.After(value = "log", onThrowable = Throwable.class) + public static void afterLog(LogRecord logRecord, @Advice.BeforeResult CallDepth otelCallDepth) { + try { + otelCallDepth.decrementAndGet(); + } catch (Throwable t) { + + } + } + +} diff --git a/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/OTFeature.java b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/OTFeature.java new file mode 100644 index 00000000..f4d8cdcc --- /dev/null +++ b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/OTFeature.java @@ -0,0 +1,46 @@ +package com.alibaba.jvm; + +import io.opentelemetry.javaagent.bootstrap.executors.PropagatedContext; +import io.opentelemetry.javaagent.tooling.field.GeneratedVirtualFieldNamesDelegate; +import org.graalvm.collections.Pair; +import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.nativeimage.hosted.RuntimeReflection; + +import java.lang.reflect.Method; +import java.util.concurrent.Callable; +import java.util.concurrent.ForkJoinTask; +import java.util.concurrent.Future; + +/** + * Register reflections used in OT code like: + *
+ *      VirtualField, PropagatedContext> virtualField = VirtualField.find(Callable.class, PropagatedContext.class);
+ * 
+ * The {@code find} method uses reflection. But it can be replaced to reflection result by OT Gradle build plugin at jar build time. + */ +public class OTFeature implements Feature { + + private static final Pair, Class>[] virtualFieldClassPairs = new Pair[]{ + /** from {@link ExecutorsAspect} */ + Pair.create(Runnable.class, PropagatedContext.class), + Pair.create(ForkJoinTask.class, PropagatedContext.class), + Pair.create(Future.class, PropagatedContext.class), + Pair.create(Callable.class, PropagatedContext.class) + }; + + @Override + public void beforeAnalysis(BeforeAnalysisAccess a) { + for (Pair, Class> virtualFieldClassPair : virtualFieldClassPairs) { + String virtualFieldClassName = GeneratedVirtualFieldNamesDelegate.getVirtualFieldImplementationClassName(virtualFieldClassPair.getLeft().getTypeName(), + virtualFieldClassPair.getRight().getTypeName()); + try { + Class c = a.findClassByName(virtualFieldClassName); + RuntimeReflection.register(c); + Method m = c.getDeclaredMethod("getVirtualField", Class.class, Class.class); + RuntimeReflection.register(m); + } catch (ReflectiveOperationException e) { + System.out.println("Warning: Can't find " + virtualFieldClassName + "#getVirtualField(Class,Class) method"); + } + } + } +} diff --git a/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/RunnableAspect.java b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/RunnableAspect.java new file mode 100644 index 00000000..1010d58c --- /dev/null +++ b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/RunnableAspect.java @@ -0,0 +1,40 @@ +package com.alibaba.jvm; + +import com.oracle.svm.core.annotate.Advice; +import com.oracle.svm.core.annotate.Aspect; +import io.opentelemetry.javaagent.bootstrap.executors.PropagatedContext; +import io.opentelemetry.javaagent.bootstrap.executors.TaskAdviceHelper; +import io.opentelemetry.javaagent.shaded.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.shaded.io.opentelemetry.context.Scope; + +/** + * This is the native image version of + * {@link io.opentelemetry.javaagent.instrumentation.executors.RunnableInstrumentation}. + */ +@Aspect(implementInterface = "java.lang.Runnable", onlyWith = Aspect.JDKClassOnly.class) +public class RunnableAspect { + + @Advice.Before("run") + public static Scope beforeRun(@Advice.This Runnable thiz) { + try { + VirtualField virtualField = VirtualField.find(Runnable.class, + PropagatedContext.class); + return TaskAdviceHelper.makePropagatedContextCurrent(virtualField, thiz); + } catch (Throwable t) { + return null; + } + + } + + @Advice.After(value = "run", onThrowable = Throwable.class) + public static void afterRun(@Advice.BeforeResult Scope scope) { + try { + if (scope != null) { + scope.close(); + } + } catch (Throwable t) { + + } + + } +} diff --git a/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/Target_io_opentelemetry_javaagent_OpenTelemetryAgent.java b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/Target_io_opentelemetry_javaagent_OpenTelemetryAgent.java new file mode 100644 index 00000000..9966bc53 --- /dev/null +++ b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/Target_io_opentelemetry_javaagent_OpenTelemetryAgent.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.alibaba.jvm; + +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; +import io.opentelemetry.javaagent.OpenTelemetryAgent; +import io.opentelemetry.javaagent.bootstrap.AgentInitializer; + +import java.lang.instrument.Instrumentation; + +@SuppressWarnings("OtelPrivateConstructorForUtilityClass") +@TargetClass(OpenTelemetryAgent.class) +public final class Target_io_opentelemetry_javaagent_OpenTelemetryAgent { + + @SuppressWarnings("UnusedMethod") + @Substitute + private static void startAgent(Instrumentation inst, boolean fromPremain) { + try { + AgentInitializer.initialize(inst, null, fromPremain); + } catch (Throwable ex) { + // Don't rethrow. We don't have a log manager here, so just print. + System.err.println("ERROR " + OpenTelemetryAgent.class.getName()); + ex.printStackTrace(); + } + } +} diff --git a/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/Target_io_opentelemetry_javaagent_bootstrap_AgentInitializer.java b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/Target_io_opentelemetry_javaagent_bootstrap_AgentInitializer.java new file mode 100644 index 00000000..fc0c9611 --- /dev/null +++ b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/Target_io_opentelemetry_javaagent_bootstrap_AgentInitializer.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.alibaba.jvm; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; +import io.opentelemetry.javaagent.bootstrap.AgentInitializer; +import io.opentelemetry.javaagent.bootstrap.AgentStarter; + +import java.io.File; +import java.lang.instrument.Instrumentation; + +@SuppressWarnings("OtelPrivateConstructorForUtilityClass") +@TargetClass(AgentInitializer.class) +public final class Target_io_opentelemetry_javaagent_bootstrap_AgentInitializer { + @Alias private static ClassLoader agentClassLoader; + + @Alias private static AgentStarter agentStarter; + + @SuppressWarnings("Unused") + @Substitute + public static void initialize(Instrumentation inst, File javaagentFile, boolean fromPremain) { + if (agentClassLoader != null) { + return; + } + agentClassLoader = ClassLoader.getSystemClassLoader(); + agentStarter = createAgentStarter(agentClassLoader, inst, javaagentFile); + agentStarter.start(); + } + + @Alias(noSubstitution = true) + private static native AgentStarter createAgentStarter( + ClassLoader agentClassLoader, Instrumentation instrumentation, File javaagentFile); +} diff --git a/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/Target_io_opentelemetry_javaagent_tooling_AgentInstaller.java b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/Target_io_opentelemetry_javaagent_tooling_AgentInstaller.java new file mode 100644 index 00000000..80fc7ad4 --- /dev/null +++ b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/Target_io_opentelemetry_javaagent_tooling_AgentInstaller.java @@ -0,0 +1,134 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.alibaba.jvm; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; +import io.opentelemetry.javaagent.bootstrap.InstrumentedTaskClasses; +import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.javaagent.extension.AgentListener; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesConfigurer; +import io.opentelemetry.javaagent.tooling.AgentExtension; +import io.opentelemetry.javaagent.tooling.AgentInstaller; +import io.opentelemetry.javaagent.tooling.BeforeAgentListener; +import io.opentelemetry.javaagent.tooling.asyncannotationsupport.WeakRefAsyncOperationEndStrategies; +import io.opentelemetry.javaagent.tooling.config.ConfigPropertiesBridge; +import io.opentelemetry.javaagent.tooling.config.EarlyInitAgentConfig; +import io.opentelemetry.javaagent.tooling.ignore.IgnoredTypesBuilderImpl; +import io.opentelemetry.javaagent.tooling.util.Trie; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; + +import java.lang.instrument.Instrumentation; +import java.util.List; + +import static io.opentelemetry.javaagent.tooling.OpenTelemetryInstaller.installOpenTelemetrySdk; +import static io.opentelemetry.javaagent.tooling.SafeServiceLoader.loadOrdered; +import static java.util.logging.Level.FINE; +import static java.util.logging.Level.SEVERE; + +@SuppressWarnings("OtelPrivateConstructorForUtilityClass") +@TargetClass(AgentInstaller.class) +public final class Target_io_opentelemetry_javaagent_tooling_AgentInstaller { + @SuppressWarnings("ConstantField") + @Alias + static String JAVAAGENT_ENABLED_CONFIG; + + @Alias private static io.opentelemetry.javaagent.bootstrap.PatchLogger logger; + + @Substitute + public static void installBytebuddyAgent( + Instrumentation inst, ClassLoader extensionClassLoader, EarlyInitAgentConfig earlyConfig) { + logVersionInfo(); + if (earlyConfig.getBoolean(JAVAAGENT_ENABLED_CONFIG, true)) { + List agentListeners = loadOrdered(AgentListener.class, extensionClassLoader); + installBytebuddyAgent(inst, extensionClassLoader, agentListeners); + } else { + logger.fine("Tracing is disabled, not installing instrumentations."); + } + } + + @Substitute + private static void installBytebuddyAgent( + Instrumentation inst, + ClassLoader extensionClassLoader, + Iterable agentListeners) { + WeakRefAsyncOperationEndStrategies.initialize(); + + // If noop OpenTelemetry is enabled, autoConfiguredSdk will be null and AgentListeners are not + // called + AutoConfiguredOpenTelemetrySdk autoConfiguredSdk = + installOpenTelemetrySdk(extensionClassLoader); + + ConfigProperties sdkConfig = AutoConfigureUtil.getConfig(autoConfiguredSdk); + InstrumentationConfig.internalInitializeConfig(new ConfigPropertiesBridge(sdkConfig)); + copyNecessaryConfigToSystemProperties(sdkConfig); + + setBootstrapPackages(sdkConfig, extensionClassLoader); + /*ConfiguredResourceAttributesHolder.initialize( + SdkAutoconfigureAccess.getResourceAttributes(autoConfiguredSdk));*/ + + for (BeforeAgentListener agentListener : + loadOrdered(BeforeAgentListener.class, extensionClassLoader)) { + agentListener.beforeAgent(autoConfiguredSdk); + } + + int numberOfLoadedExtensions = 0; + for (AgentExtension agentExtension : loadOrdered(AgentExtension.class, extensionClassLoader)) { + if (logger.isLoggable(FINE)) { + logger.log( + FINE, + "Loading extension {0} [class {1}]", + new Object[] {agentExtension.extensionName(), agentExtension.getClass().getName()}); + } + try { + numberOfLoadedExtensions++; + } catch (Exception | LinkageError e) { + logger.log( + SEVERE, + "Unable to load extension " + + agentExtension.extensionName() + + " [class " + + agentExtension.getClass().getName() + + "]", + e); + } + } + logger.log(FINE, "Installed {0} extension(s)", numberOfLoadedExtensions); + // configureIgnoredTypes here + IgnoredTypesBuilderImpl builder = new IgnoredTypesBuilderImpl(); + for (IgnoredTypesConfigurer configurer : + loadOrdered(IgnoredTypesConfigurer.class, extensionClassLoader)) { + configurer.configure(builder, sdkConfig); + } + + Trie ignoredTasksTrie = builder.buildIgnoredTasksTrie(); + InstrumentedTaskClasses.setIgnoredTaskClassesPredicate(ignoredTasksTrie::contains); + // configureIgnoredTypes + addHttpServerResponseCustomizers(extensionClassLoader); + + runAfterAgentListeners(agentListeners, autoConfiguredSdk); + } + + @Alias + private static native void logVersionInfo(); + + @Alias + private static native void copyNecessaryConfigToSystemProperties(ConfigProperties config); + + @Alias + private static native void setBootstrapPackages( + ConfigProperties config, ClassLoader extensionClassLoader); + + @Alias + private static native void addHttpServerResponseCustomizers(ClassLoader extensionClassLoader); + + @Alias + private static native void runAfterAgentListeners( + Iterable agentListeners, AutoConfiguredOpenTelemetrySdk autoConfiguredSdk); +} diff --git a/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/Target_io_opentelemetry_javaagent_tooling_AgentStarterImpl.java b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/Target_io_opentelemetry_javaagent_tooling_AgentStarterImpl.java new file mode 100644 index 00000000..52d9077c --- /dev/null +++ b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/Target_io_opentelemetry_javaagent_tooling_AgentStarterImpl.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.alibaba.jvm; + +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; +import io.opentelemetry.javaagent.tooling.AgentStarterImpl; +import io.opentelemetry.javaagent.tooling.config.EarlyInitAgentConfig; + +@TargetClass(AgentStarterImpl.class) +public final class Target_io_opentelemetry_javaagent_tooling_AgentStarterImpl { + + @SuppressWarnings({"MethodCanBeStatic", "UnusedVariable", "Unused"}) + @Substitute + private ClassLoader createExtensionClassLoader( + ClassLoader agentClassLoader, EarlyInitAgentConfig earlyConfig) { + return ClassLoader.getSystemClassLoader(); + } +} diff --git a/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/Target_java_lang_Class.java b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/Target_java_lang_Class.java new file mode 100644 index 00000000..4ca1860e --- /dev/null +++ b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/com/alibaba/jvm/Target_java_lang_Class.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.alibaba.jvm; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; +import com.oracle.svm.core.annotate.TargetElement; +import io.opentelemetry.javaagent.instrumentation.internal.reflection.ReflectionHelper; + +/** + * This class is the native support version of {@link + * io.opentelemetry.javaagent.instrumentation.internal.reflection.ClassInstrumentation}.
+ * The {@code ClassInstrumentation} class transforms {@link Class} at Java program + * runtime. This class do the same transform at native image build time. + */ +@TargetClass(Class.class) +public final class Target_java_lang_Class { + + @Alias(noSubstitution = true) + @TargetElement(name = "getInterfaces") + private native Class[] originalGetInterfaces(boolean clone); + + /** + * This substituted method filters the result of original method.
+ * {@code (Class)(Object)this} makes javac happy. At javac compile time, {@code this} is {@code + * Target_java_lang_Class}. At runtime, {@code this} is the original class, i.e. {@code + * java.lang.Class}. + */ + @Substitute + private Class[] getInterfaces(boolean clone) { + return ReflectionHelper.filterInterfaces( + originalGetInterfaces(clone), (Class) (Object) this); + } +} diff --git a/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/io/opentelemetry/javaagent/tooling/field/GeneratedVirtualFieldNamesDelegate.java b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/io/opentelemetry/javaagent/tooling/field/GeneratedVirtualFieldNamesDelegate.java new file mode 100644 index 00000000..1a32c37d --- /dev/null +++ b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/java/io/opentelemetry/javaagent/tooling/field/GeneratedVirtualFieldNamesDelegate.java @@ -0,0 +1,7 @@ +package io.opentelemetry.javaagent.tooling.field; + +public class GeneratedVirtualFieldNamesDelegate { + public static String getVirtualFieldImplementationClassName(String typeName, String fieldTypeName){ + return GeneratedVirtualFieldNames.getVirtualFieldImplementationClassName(typeName,fieldTypeName); + } +} diff --git a/graalvm-native-image-demo/opentelemetry-agent-native/src/main/resources/META-INF/native-image/native-image.properties b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/resources/META-INF/native-image/native-image.properties new file mode 100644 index 00000000..97a38417 --- /dev/null +++ b/graalvm-native-image-demo/opentelemetry-agent-native/src/main/resources/META-INF/native-image/native-image.properties @@ -0,0 +1,2 @@ +Args=\ + --features=com.alibaba.jvm.OTFeature \ No newline at end of file diff --git a/graalvm-native-image-demo/ot-demo/spring-boot/README.md b/graalvm-native-image-demo/ot-demo/spring-boot/README.md new file mode 100644 index 00000000..f5ab4337 --- /dev/null +++ b/graalvm-native-image-demo/ot-demo/spring-boot/README.md @@ -0,0 +1,36 @@ +# Getting Started +This project demonstrates the agent support ability of GraalVM native image enforced by the [agent support PR](https://github.com/oracle/graal/pull/8077). + +The base project is the [Spring native demo project](https://start.spring.io/#!type=maven-project&language=java&platformVersion=3.2.1&packaging=jar&jvmVersion=17&groupId=com.example&artifactId=demo&name=demo&description=Demo%20project%20for%20Spring%20Boot&packageName=com.example.demo&dependencies=native). + +When gets started, it responds "Hello World!" to a request to localhost:2327. +This project starts along with OpenTelemetry java agent instrumentation as described here: https://opentelemetry.io/docs/instrumentation/java/getting-started/#instrumentation. The monitoring data can be collected by log or other supported means. +## Methodology +To support agent instrumentation in native image, actions should be taken from two sides: +1. GraalVM side: must support compiling the instrumented code into the native image. It is supported by the [agent support PR](https://github.com/oracle/graal/pull/8077). +2. Agent side: agent developer must define agent's behaviors in native image. Such as the `premain` behaviors and JDK class transformations. [Opentelemetry-agent-native project](../opentelemetry-agent-native/) gives an example of how such adaption is done for OpenTelemetry agent. + +## Run Native Image with Agent Instrumentation +### Prequisite + + - Build the `agent support PR` to get the GraalVM JDK. + - Or download the [dev version [binary](https://github.com/ziyilin/ziyi-forked-graal/releases/tag/static-instrument) for the experiment only. + - Make sure your `GRAALVM_HOME` system variable refers to one of the above GraalVM releases. + +### Steps +1. cd to the root of this project +2. `mvn package`: Build the jar package of this project +3. `./run.sh --jvm --collect --log`: Execute the project in JVM mode, enabling OpenTelemetry agent to log monitoring data. Meanwhile, native-image-agent is enabled to collect transformation classes and other dynamic data. When this step ends, data is dumped to `native-configs` directory. +4. `./build.sh`: Build the native image. +5. `./run.sh --svm --log`: Run the native image application. +6. `curl localhost:2327`: Send a request to the application. + +### Alternatives +In the above steps, the OT agent prints monitoring data in the console. In the more practical scenarios, the data is collected and sent to the monitoring platform such as ARMS (Application Real-Time Monitoring Service) from Alibaba Cloud. + +This demo project supports such a scenario as well: +1. Set your system variables `ARMS_AUTH` and `ARMS_ENDPOINT`. `ARMS_AUTH` is your token, it looks like `b590lhguqs@3a7*********9b_b590lhguqs@53d*****8301`. `ARMS_ENDPOINT` is your connection endpoint, it looks like `http://tracing-analysis-dc-bj.aliyuncs.com:8090`. These two values can be obtained from your control panel if you're already a customer of ARMS. The service name by default is "native-agent-demo". It can be modified by editing the `run.sh` script. +2. For the above step 3 and 5, replace `--log` with `--arms`. + +The following diagram illustrates the result shown in the ARMS platform. +![alt text](images/image.png) \ No newline at end of file diff --git a/graalvm-native-image-demo/ot-demo/spring-boot/access-filter-file.json b/graalvm-native-image-demo/ot-demo/spring-boot/access-filter-file.json new file mode 100644 index 00000000..0f0da3a1 --- /dev/null +++ b/graalvm-native-image-demo/ot-demo/spring-boot/access-filter-file.json @@ -0,0 +1,4 @@ +{ "rules": [ + {"excludeClasses": "sun.launcher.LauncherHelper"} + ] +} diff --git a/graalvm-native-image-demo/ot-demo/spring-boot/build.sh b/graalvm-native-image-demo/ot-demo/spring-boot/build.sh new file mode 100755 index 00000000..012fbda9 --- /dev/null +++ b/graalvm-native-image-demo/ot-demo/spring-boot/build.sh @@ -0,0 +1,24 @@ +#!/bin/bash +NATIVE_LIBS_DIR="ot-native-support-libs" +INST_JAR="instrument.jar" +# NATIVE_LIBS_DIR and ot agent jar should be created when running 'mvn package' previously +pushd $NATIVE_LIBS_DIR +if [ ! -f $INST_JAR ]; then + jar xf opentelemetry-javaagent-1.32.0.jar inst/* -d inst + + pushd inst + find . -type f -name "*.classdata" | while read -r file; do + mv "$file" "${file%.classdata}.class" + done + jar cf ../$INST_JAR * + popd + + mvn install:install-file -Dfile=$INST_JAR -DgroupId=io.opentelemetry.javaagent -DartifactId=opentelemetry-javaagent-inst-native-support -Dversion=1.32.0 -Dpackaging=jar +fi +popd + +pushd ../../opentelemetry-agent-native +mvn install +popd + +mvn -Pnative package diff --git a/graalvm-native-image-demo/ot-demo/spring-boot/images/image.png b/graalvm-native-image-demo/ot-demo/spring-boot/images/image.png new file mode 100644 index 00000000..52f96e0f Binary files /dev/null and b/graalvm-native-image-demo/ot-demo/spring-boot/images/image.png differ diff --git a/graalvm-native-image-demo/ot-demo/spring-boot/pom.xml b/graalvm-native-image-demo/ot-demo/spring-boot/pom.xml new file mode 100644 index 00000000..2b2b560f --- /dev/null +++ b/graalvm-native-image-demo/ot-demo/spring-boot/pom.xml @@ -0,0 +1,124 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.1 + + + com.alibaba.jvm + spring-ot-demo + 1.0.0-SNAPSHOT + spring-ot-demo + Demo project for native Spring Boot with Opentelemetry agent + + 17 + 1.32.0 + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-dependency-plugin + 3.6.1 + + + copy + package + + copy + + + + + io.opentelemetry.javaagent + opentelemetry-javaagent + ${ot.version} + ot-native-support-libs + + + + + + + + + + + native + + + + org.graalvm.buildtools + native-maven-plugin + true + + + build-native + + compile-no-fork + + package + + + + false + + -H:ConfigurationFileDirectories=native-configs + -H:+ReportExceptionStackTraces + + + + + + + + + io.opentelemetry.javaagent + opentelemetry-javaagent + ${ot.version} + + + io.opentelemetry.javaagent + opentelemetry-javaagent-inst-native-support + ${ot.version} + + + com.alibaba.jvm + opentelemetry-agent-native + 1.0-SNAPSHOT + + + + + + + diff --git a/graalvm-native-image-demo/ot-demo/spring-boot/run.sh b/graalvm-native-image-demo/ot-demo/spring-boot/run.sh new file mode 100755 index 00000000..af05db5c --- /dev/null +++ b/graalvm-native-image-demo/ot-demo/spring-boot/run.sh @@ -0,0 +1,81 @@ +#!/bin/bash +JAVA="$GRAALVM_HOME" +j=0 +NUM=$# + +while [ $j -lt $NUM ]; do + case $1 in + "--svm") + VM_MODE="svm" + shift 1 + j=$(($j + 1)) + ;; + "--jvm") + VM_MODE="jvm" + shift 1 + j=$(($j + 1)) + ;; + "--collect") + COLLECT="true" + shift 1 + j=$(($j + 1)) + ;; + "--arms") + LOG_MODE="arms" + shift 1 + j=$(($j + 1)) + ;; + "--log") + LOG_MODE="log" + shift 1 + j=$(($j + 1)) + ;; + *) + echo "option $1 is not supported" + printUsage + exit 1 + ;; + esac +done + +if [ x"$LOG_MODE" == "xlog" ]; then + export OTEL_TRACES_EXPORTER=logging + export OTEL_METRICS_EXPORTER=logging + export OTEL_LOGS_EXPORTER=logging +elif [ x"$LOG_MODE" == "xarms" ]; then + LOG_PROPERTIES="-Dotel.resource.attributes=service.name=native-agent-demo \ +-Dotel.exporter.otlp.headers=Authentication=$ARMS_AUTH \ +-Dotel.exporter.otlp.endpoint=$ARMS_ENDPOINT \ +-Dotel.metrics.exporter=none" +else + echo "Must specify logging mode. It can be --arms, or --log." + exit 1 +fi + +export PORT="2327" + +function curlService(){ + while true; do + result=$(netstat -tln | grep ":${PORT} ") + if [ -n "$result" ]; then + curl localhost:${PORT} + pid=`lsof -i :${PORT} | grep LISTEN | awk '{print $2}'` + kill -2 $pid + break + fi + sleep 1 + done +} + +if [ x"$VM_MODE" == "xjvm" ]; then + if [ x"$COLLECT" == "xtrue" ]; then + NATIVE_IMAGE_AGENT="-agentpath:$JAVA/lib/libnative-image-agent.so=access-filter-file=access-filter-file.json,no-builtin-heuristic-filter,config-output-dir=native-configs,experimental-instrument,experimental-class-define-support,predefine-class-rules=io/opentelemetry/javaagent/bootstrap/field/VirtualFieldImpl" + curlService& + fi + $JAVA/bin/java $NATIVE_IMAGE_AGENT $LOG_PROPERTIES -javaagent:ot-native-support-libs/opentelemetry-javaagent-1.32.0.jar -jar target/spring-ot-demo-1.0.0-SNAPSHOT.jar +elif [ x"$VM_MODE" == "xsvm" ]; then + ./target/spring-ot-demo $LOG_PROPERTIES +else + echo "Unrecognized VM mode, Only support --jvm and --svm" + exit 1 +fi diff --git a/graalvm-native-image-demo/ot-demo/spring-boot/src/main/java/com/example/demo/DemoApplication.java b/graalvm-native-image-demo/ot-demo/spring-boot/src/main/java/com/example/demo/DemoApplication.java new file mode 100644 index 00000000..67fbb078 --- /dev/null +++ b/graalvm-native-image-demo/ot-demo/spring-boot/src/main/java/com/example/demo/DemoApplication.java @@ -0,0 +1,20 @@ +package com.example.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RequestMapping; + +@RestController +@SpringBootApplication +public class DemoApplication { + @RequestMapping("/") + String home() { + return "Hello World!"; + } + + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } + +} diff --git a/graalvm-native-image-demo/ot-demo/spring-boot/src/main/resources/application.properties b/graalvm-native-image-demo/ot-demo/spring-boot/src/main/resources/application.properties new file mode 100644 index 00000000..2c5e1840 --- /dev/null +++ b/graalvm-native-image-demo/ot-demo/spring-boot/src/main/resources/application.properties @@ -0,0 +1 @@ +server.port=2327 diff --git a/graalvm-native-image-demo/ot-demo/spring-boot/src/test/java/com/example/demo/DemoApplicationTests.java b/graalvm-native-image-demo/ot-demo/spring-boot/src/test/java/com/example/demo/DemoApplicationTests.java new file mode 100644 index 00000000..2778a6a7 --- /dev/null +++ b/graalvm-native-image-demo/ot-demo/spring-boot/src/test/java/com/example/demo/DemoApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.demo; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class DemoApplicationTests { + + @Test + void contextLoads() { + } + +}