Skip to content

Commit a0bf8b9

Browse files
authored
Add GraalVM native image demo for agent support (#200)
1 parent 2717ec3 commit a0bf8b9

27 files changed

+1312
-0
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# About
2+
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.
3+
4+
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:
5+
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.
6+
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.
7+
8+
### Native Image Runtime `premain` Support
9+
There are two approaches to define native image runtime `premain` actions.
10+
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:
11+
```java
12+
String vm = System.getProperty("java.vm.name");
13+
if ("Substrate VM".equals(vm)) {
14+
// In native image
15+
...
16+
} else {
17+
// In JVM
18+
...
19+
}
20+
```
21+
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.
22+
23+
## Enhance Classes with Advice Annotations
24+
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.
25+
26+
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.
27+
The basic rules are:
28+
29+
- Class annotation `@Aspect` defines which classes will be enhanced. See the [source](https://github.com/oracle/graal/pull/8077/files#diff-2f2c248dd98c839fd85314a71d90f0d435773c3f40fc8a388ed4a9bb50440a6e) for details.
30+
- 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.
31+
32+
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.
33+
34+
### Exemptions of JDK Methods
35+
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.
36+
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.
37+
38+
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).
39+
40+
Here are examples of how to use the advice annotations.
41+
### Declare classes to be instrumented
42+
@Aspect annotation has 3 methods to declare different kinds of class-matching strategies:
43+
1. `subClassOf`: match all subclasses of the specified class. See [LoggerAspect.java](src/main/java/com/alibaba/jvm/LoggerAspect.java) for details.
44+
2. `implementInterface`: match all implementation classes of the specified class. See [CallableAspect.java](src/main/java/com/alibaba/jvm/CallableAspect.java) for details.
45+
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.
46+
47+
### Declare Advice Methods
48+
There are two kinds of advice methods, before and after, executing before and after the target method is invoked.
49+
By default, the target method is matched by the annotated method's name and parameters.
50+
51+
The advice methods must **public static**.
52+
53+
Here are examples of how to use advice method annotations:
54+
55+
- Match multiple methods with the same parameters: all methods in [ExecutorsAspect](src/main/java/com/alibaba/jvm/ExecutorsAspect.java)
56+
- Rewrite input parameter: `beforeExecute` in [ExecutorsAspect](src/main/java/com/alibaba/jvm/ExecutorsAspect.java)
57+
- Restrict returning type: `exitSubmitRunnable` in [ExecutorsAspect](src/main/java/com/alibaba/jvm/ExecutorsAspect.java)
58+
- Exception interception: `exitSubmitRunnable` in [ExecutorsAspect](src/main/java/com/alibaba/jvm/ExecutorsAspect.java)
59+
- Pass returning value from before method to after method: `enterSubmitCallable` and `exitSubmitCallable` in [ExecutorsAspect](src/main/java/com/alibaba/jvm/ExecutorsAspect.java)
60+
- Refer to `this` in advice methods: `beforeRun` in [RunnableAspect](src/main/java/com/alibaba/jvm/RunnableAspect.java)
61+
- Refer to the field of target method's class: Not supported by annotations, need to use reflection.
62+
63+
### Extra Reflection Registration
64+
[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.
65+
``` java
66+
VirtualField<Callable<?>, PropagatedContext> virtualField = VirtualField.find(Callable.class, PropagatedContext.class);
67+
```
68+
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.
69+
70+
[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.
71+
72+
### Shaded Class Dependency
73+
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.
Binary file not shown.
Binary file not shown.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
4+
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<groupId>com.alibaba.jvm</groupId>
8+
<artifactId>opentelemetry-agent-native</artifactId>
9+
<packaging>jar</packaging>
10+
<version>1.0-SNAPSHOT</version>
11+
<properties>
12+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
13+
<ot.version>1.32.0</ot.version>
14+
<ot.alpha.version>${ot.version}-alpha</ot.alpha.version>
15+
</properties>
16+
17+
<dependencies>
18+
<dependency>
19+
<groupId>io.opentelemetry.javaagent</groupId>
20+
<artifactId>opentelemetry-javaagent</artifactId>
21+
<version>${ot.version}</version>
22+
</dependency>
23+
<dependency>
24+
<groupId>io.opentelemetry.javaagent</groupId>
25+
<artifactId>opentelemetry-javaagent-extension-api</artifactId>
26+
<version>${ot.alpha.version}</version>
27+
</dependency>
28+
<dependency>
29+
<groupId>io.opentelemetry.javaagent</groupId>
30+
<artifactId>opentelemetry-javaagent-tooling</artifactId>
31+
<version>${ot.alpha.version}</version>
32+
</dependency>
33+
<dependency>
34+
<groupId>io.opentelemetry</groupId>
35+
<artifactId>opentelemetry-sdk-extension-autoconfigure</artifactId>
36+
<version>${ot.version}</version>
37+
</dependency>
38+
<dependency>
39+
<groupId>io.opentelemetry.javaagent</groupId>
40+
<artifactId>opentelemetry-javaagent-inst-native-support</artifactId>
41+
<version>${ot.version}</version>
42+
</dependency>
43+
<dependency>
44+
<groupId>io.opentelemetry.instrumentation</groupId>
45+
<artifactId>opentelemetry-instrumentation-annotations-support</artifactId>
46+
<version>${ot.alpha.version}</version>
47+
</dependency>
48+
49+
<!-- GraalVM for JDK 21 usage-->
50+
<dependency>
51+
<groupId>org.graalvm.sdk</groupId>
52+
<artifactId>nativeimage</artifactId>
53+
<!--Require classes introduced by static agent instrument PR -->
54+
<version>24.1.0-instru-dev</version>
55+
<scope>system</scope>
56+
<systemPath>${basedir}/libs/nativeimage.jar</systemPath>
57+
</dependency>
58+
<dependency>
59+
<groupId>org.graalvm.sdk</groupId>
60+
<artifactId>collections</artifactId>
61+
<version>23.1.2</version>
62+
</dependency>
63+
<!--GraalVM for JDK 21 usage-->
64+
65+
<!-- GraalVM for JDK 17 usage-->
66+
<!--<dependency>
67+
<groupId>org.graalvm.sdk</groupId>
68+
<artifactId>graal-sdk</artifactId>
69+
<version>23.0.2-instru-dev</version>
70+
<scope>system</scope>
71+
<systemPath>${basedir}/libs/graal-sdk.jar</systemPath>
72+
</dependency>-->
73+
<!-- GraalVM for JDK 17 usage-->
74+
</dependencies>
75+
76+
<build>
77+
<plugins>
78+
<plugin>
79+
<groupId>org.apache.maven.plugins</groupId>
80+
<artifactId>maven-compiler-plugin</artifactId>
81+
<version>3.12.1</version>
82+
<configuration>
83+
<release>17</release>
84+
</configuration>
85+
</plugin>
86+
<plugin>
87+
<groupId>org.apache.maven.plugins</groupId>
88+
<artifactId>maven-resources-plugin</artifactId>
89+
<version>3.3.1</version>
90+
<configuration>
91+
<encoding>UTF-8</encoding>
92+
</configuration>
93+
</plugin>
94+
</plugins>
95+
</build>
96+
</project>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.alibaba.jvm;
2+
3+
import com.oracle.svm.core.annotate.Advice;
4+
import com.oracle.svm.core.annotate.Aspect;
5+
import io.opentelemetry.javaagent.bootstrap.executors.PropagatedContext;
6+
import io.opentelemetry.javaagent.bootstrap.executors.TaskAdviceHelper;
7+
import io.opentelemetry.javaagent.shaded.instrumentation.api.util.VirtualField;
8+
import io.opentelemetry.javaagent.shaded.io.opentelemetry.context.Scope;
9+
10+
import java.util.concurrent.Callable;
11+
12+
/**
13+
* Match classes implement {@link Callable}.
14+
* This is the native image version of {@link io.opentelemetry.javaagent.instrumentation.executors.CallableInstrumentation}.
15+
*/
16+
@Aspect(implementInterface = "java.util.concurrent.Callable", onlyWith = Aspect.JDKClassOnly.class)
17+
public class CallableAspect {
18+
@Advice.Before("call")
19+
public static Scope beforeCall(@Advice.This Callable<?> task) {
20+
try {
21+
VirtualField<Callable<?>, PropagatedContext> virtualField =
22+
VirtualField.find(Callable.class, PropagatedContext.class);
23+
return TaskAdviceHelper.makePropagatedContextCurrent(virtualField, task);
24+
} catch (Throwable t) {
25+
return null;
26+
}
27+
}
28+
29+
@Advice.After(value = "call", onThrowable = Throwable.class)
30+
public static void afterCall(@Advice.BeforeResult Scope scope) {
31+
try {
32+
if (scope != null) {
33+
scope.close();
34+
}
35+
} catch (Throwable t) {
36+
//do nothing
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)