This is an automated email from the ASF dual-hosted git repository. wusheng pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/skywalking-graalvm-distro.git
commit 612e571b5ac45085e8722b53b8f0add1904c21c6 Author: Wu Sheng <[email protected]> AuthorDate: Wed Feb 18 18:17:40 2026 +0800 Add build-time OAL class export tool Run the OAL engine at build time to export generated .class files for GraalVM native image compatibility (no runtime bytecode generation). OALClassExporter: - Leverages existing SW_OAL_ENGINE_DEBUG mechanism in OALClassGeneratorV2 to export .class files via ctClass.toBytecode() - Processes all 9 OALDefine instances (Disable, Core, JVM, CLR, Browser, Mesh, EBPF, TCP, Cilium) - Initializes DefaultScopeDefine via AnnotationScan before running engines - Loads OAL scripts from skywalking submodule via additionalClasspathElements - Writes manifests: oal-metrics-classes.txt (620), oal-dispatcher-classes.txt (45), oal-disabled-sources.txt - Packages generated classes into classified JAR (oal-exporter-*-generated.jar) Tests: - Validates ALL_DEFINES covers all 9 expected OAL scripts - Verifies all OAL scripts are resolvable on classpath - Runs full export and asserts metrics, dispatchers, builders are generated Co-Authored-By: Claude Opus 4.6 <[email protected]> --- build-tools/oal-exporter/pom.xml | 152 ++++++++++++++++ .../server/buildtools/oal/OALClassExporter.java | 201 +++++++++++++++++++++ .../buildtools/oal/OALClassExporterTest.java | 130 +++++++++++++ 3 files changed, 483 insertions(+) diff --git a/build-tools/oal-exporter/pom.xml b/build-tools/oal-exporter/pom.xml new file mode 100644 index 0000000..d53d20e --- /dev/null +++ b/build-tools/oal-exporter/pom.xml @@ -0,0 +1,152 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.apache.skywalking</groupId> + <artifactId>build-tools</artifactId> + <version>1.0.0-SNAPSHOT</version> + </parent> + + <artifactId>oal-exporter</artifactId> + <name>OAL Exporter</name> + <description>Runs OAL engine at build time and exports generated .class files</description> + + <dependencies> + <!-- OAL engine --> + <dependency> + <groupId>org.apache.skywalking</groupId> + <artifactId>oal-rt</artifactId> + </dependency> + <dependency> + <groupId>org.apache.skywalking</groupId> + <artifactId>server-core</artifactId> + </dependency> + + <!-- Receiver plugins containing OALDefine subclasses --> + <dependency> + <groupId>org.apache.skywalking</groupId> + <artifactId>skywalking-jvm-receiver-plugin</artifactId> + </dependency> + <dependency> + <groupId>org.apache.skywalking</groupId> + <artifactId>skywalking-clr-receiver-plugin</artifactId> + </dependency> + <dependency> + <groupId>org.apache.skywalking</groupId> + <artifactId>skywalking-browser-receiver-plugin</artifactId> + </dependency> + <dependency> + <groupId>org.apache.skywalking</groupId> + <artifactId>skywalking-mesh-receiver-plugin</artifactId> + </dependency> + <dependency> + <groupId>org.apache.skywalking</groupId> + <artifactId>skywalking-ebpf-receiver-plugin</artifactId> + </dependency> + <dependency> + <groupId>org.apache.skywalking</groupId> + <artifactId>envoy-metrics-receiver-plugin</artifactId> + </dependency> + + <!-- Fetcher plugin containing CiliumOALDefine --> + <dependency> + <groupId>org.apache.skywalking</groupId> + <artifactId>cilium-fetcher-plugin</artifactId> + </dependency> + + <!-- Lombok --> + <dependency> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + </dependency> + + <!-- Test --> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <!-- Run OALClassExporter at build time --> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>exec-maven-plugin</artifactId> + <version>3.5.0</version> + <executions> + <execution> + <id>export-oal-classes</id> + <phase>process-classes</phase> + <goals> + <goal>java</goal> + </goals> + <configuration> + <mainClass>org.apache.skywalking.oap.server.buildtools.oal.OALClassExporter</mainClass> + <arguments> + <argument>${project.build.directory}/generated-oal-classes</argument> + </arguments> + <!-- Load OAL scripts directly from skywalking submodule --> + <additionalClasspathElements> + <additionalClasspathElement>${project.basedir}/../../skywalking/oap-server/server-starter/src/main/resources</additionalClasspathElement> + </additionalClasspathElements> + </configuration> + </execution> + </executions> + </plugin> + + <!-- Surefire: add OAL scripts to test classpath --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <version>3.2.5</version> + <configuration> + <additionalClasspathElements> + <additionalClasspathElement>${project.basedir}/../../skywalking/oap-server/server-starter/src/main/resources</additionalClasspathElement> + </additionalClasspathElements> + </configuration> + </plugin> + + <!-- Package generated classes into a classified JAR --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <version>3.4.2</version> + <executions> + <execution> + <id>generated-classes-jar</id> + <phase>package</phase> + <goals> + <goal>jar</goal> + </goals> + <configuration> + <classesDirectory>${project.build.directory}/generated-oal-classes</classesDirectory> + <classifier>generated</classifier> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/build-tools/oal-exporter/src/main/java/org/apache/skywalking/oap/server/buildtools/oal/OALClassExporter.java b/build-tools/oal-exporter/src/main/java/org/apache/skywalking/oap/server/buildtools/oal/OALClassExporter.java new file mode 100644 index 0000000..ded57e7 --- /dev/null +++ b/build-tools/oal-exporter/src/main/java/org/apache/skywalking/oap/server/buildtools/oal/OALClassExporter.java @@ -0,0 +1,201 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.skywalking.oap.server.buildtools.oal; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; +import lombok.extern.slf4j.Slf4j; +import org.apache.skywalking.aop.server.receiver.mesh.MeshOALDefine; +import org.apache.skywalking.oal.v2.OALEngineV2; +import org.apache.skywalking.oal.v2.generator.OALClassGeneratorV2; +import org.apache.skywalking.oap.server.core.analysis.DisableRegister; +import org.apache.skywalking.oap.server.core.annotation.AnnotationScan; +import org.apache.skywalking.oap.server.core.oal.rt.CoreOALDefine; +import org.apache.skywalking.oap.server.core.oal.rt.DisableOALDefine; +import org.apache.skywalking.oap.server.core.oal.rt.OALDefine; +import org.apache.skywalking.oap.server.core.source.DefaultScopeDefine; +import org.apache.skywalking.oap.server.core.storage.StorageBuilderFactory; +import org.apache.skywalking.oap.server.fetcher.cilium.CiliumOALDefine; +import org.apache.skywalking.oap.server.receiver.browser.provider.BrowserOALDefine; +import org.apache.skywalking.oap.server.receiver.clr.provider.CLROALDefine; +import org.apache.skywalking.oap.server.receiver.ebpf.provider.EBPFOALDefine; +import org.apache.skywalking.oap.server.receiver.envoy.TCPOALDefine; +import org.apache.skywalking.oap.server.receiver.jvm.provider.JVMOALDefine; + +/** + * Build-time tool that runs the OAL engine for all 9 OALDefine configurations, + * exports generated .class files (metrics, builders, dispatchers), and writes + * manifest files listing all generated class names. + * + * OAL script files are loaded from the skywalking submodule directly via + * additionalClasspathElements in the exec-maven-plugin configuration. + * + * Uses the existing SW_OAL_ENGINE_DEBUG export mechanism in OALClassGeneratorV2. + */ +@Slf4j +public class OALClassExporter { + + private static final String METRICS_PACKAGE = + "org.apache.skywalking.oap.server.core.source.oal.rt.metrics."; + private static final String BUILDER_PACKAGE = + "org.apache.skywalking.oap.server.core.source.oal.rt.metrics.builder."; + private static final String DISPATCHER_PACKAGE = + "org.apache.skywalking.oap.server.core.source.oal.rt.dispatcher."; + + static final OALDefine[] ALL_DEFINES = { + DisableOALDefine.INSTANCE, + CoreOALDefine.INSTANCE, + JVMOALDefine.INSTANCE, + CLROALDefine.INSTANCE, + BrowserOALDefine.INSTANCE, + MeshOALDefine.INSTANCE, + EBPFOALDefine.INSTANCE, + TCPOALDefine.INSTANCE, + CiliumOALDefine.INSTANCE + }; + + public static void main(String[] args) throws Exception { + final String outputDir = args.length > 0 + ? args[0] + : "target/generated-oal-classes"; + + log.info("OAL Class Exporter: output -> {}", outputDir); + + // Validate all OAL scripts are available on classpath before running + validateOALScripts(); + + // Initialize DefaultScopeDefine — scan @ScopeDeclaration annotations on source + // classes (Service, Endpoint, etc.) to populate the scope name → ID → columns + // registry. The OAL enricher needs this to resolve source metadata. + AnnotationScan scopeScan = new AnnotationScan(); + scopeScan.registerListener(new DefaultScopeDefine.Listener()); + scopeScan.scan(); + log.info("Initialized DefaultScopeDefine scope registry"); + + // Set generated file path so debug output lands in proper package structure. + // writeGeneratedFile() appends "/metrics/", "/metrics/builder/", "/dispatcher/" + // which matches the actual Java package sub-paths. + OALClassGeneratorV2.setGeneratedFilePath( + outputDir + "/org/apache/skywalking/oap/server/core/source/oal/rt"); + + // Skip prepareRTTempFolder() which uses WorkPath.getPath() — not available + // in build tool context. Set the static guard to true so it becomes a no-op. + Field rtFolderInitField = OALClassGeneratorV2.class.getDeclaredField( + "IS_RT_TEMP_FOLDER_INIT_COMPLETED"); + rtFolderInitField.setAccessible(true); + rtFolderInitField.set(null, true); + + // Run all 9 OAL defines + for (OALDefine define : ALL_DEFINES) { + log.info("Processing: {}", define.getConfigFile()); + OALEngineV2 engine = new OALEngineV2(define); + engine.getClassGeneratorV2().setOpenEngineDebug(true); + engine.setStorageBuilderFactory(new StorageBuilderFactory.Default()); + engine.start(OALClassExporter.class.getClassLoader()); + } + + // Scan generated .class files and build manifests + List<String> metricsClasses = scanClassNames(outputDir, "metrics", METRICS_PACKAGE); + List<String> dispatcherClasses = scanClassNames(outputDir, "dispatcher", DISPATCHER_PACKAGE); + List<String> disabledSources = getDisabledSources(); + + // Write manifest files + Path metaInf = Path.of(outputDir, "META-INF"); + Files.createDirectories(metaInf); + writeManifest(metaInf.resolve("oal-metrics-classes.txt"), metricsClasses); + writeManifest(metaInf.resolve("oal-dispatcher-classes.txt"), dispatcherClasses); + writeManifest(metaInf.resolve("oal-disabled-sources.txt"), disabledSources); + + log.info("OAL Class Exporter: done - {} metrics, {} dispatchers, {} disabled sources", + metricsClasses.size(), dispatcherClasses.size(), disabledSources.size()); + } + + /** + * Validate that all OAL script files referenced by ALL_DEFINES are + * available on the classpath. Fails fast with a clear error if any are missing. + */ + private static void validateOALScripts() { + ClassLoader cl = OALClassExporter.class.getClassLoader(); + List<String> missing = new ArrayList<>(); + for (OALDefine define : ALL_DEFINES) { + String configFile = define.getConfigFile(); + if (cl.getResource(configFile) == null) { + missing.add(configFile + " (" + define.getClass().getSimpleName() + ")"); + } + } + if (!missing.isEmpty()) { + throw new IllegalStateException( + "OAL script files not found on classpath. " + + "Ensure the skywalking submodule resource directory is on the classpath.\n" + + "Missing:\n " + String.join("\n ", missing)); + } + log.info("Validated: all {} OAL scripts found on classpath", ALL_DEFINES.length); + } + + /** + * Scan a subdirectory under the generated package path for .class files + * and compute fully-qualified class names. + */ + private static List<String> scanClassNames( + String outputDir, String subDir, String packagePrefix) throws IOException { + + Path dir = Path.of(outputDir, + "org/apache/skywalking/oap/server/core/source/oal/rt", subDir); + if (!Files.isDirectory(dir)) { + return Collections.emptyList(); + } + + List<String> classNames = new ArrayList<>(); + try (Stream<Path> files = Files.list(dir)) { + files.filter(p -> p.toString().endsWith(".class")) + .filter(p -> Files.isRegularFile(p)) + .forEach(p -> { + String fileName = p.getFileName().toString(); + String simpleName = fileName.substring(0, fileName.length() - ".class".length()); + classNames.add(packagePrefix + simpleName); + }); + } + Collections.sort(classNames); + return classNames; + } + + /** + * Read disabled source names from DisableRegister singleton via reflection. + */ + @SuppressWarnings("unchecked") + private static List<String> getDisabledSources() throws Exception { + Field field = DisableRegister.class.getDeclaredField("disableEntitySet"); + field.setAccessible(true); + Set<String> disabledSet = (Set<String>) field.get(DisableRegister.INSTANCE); + List<String> result = new ArrayList<>(disabledSet); + Collections.sort(result); + return result; + } + + private static void writeManifest(Path path, List<String> lines) throws IOException { + Files.write(path, lines, StandardCharsets.UTF_8); + } +} diff --git a/build-tools/oal-exporter/src/test/java/org/apache/skywalking/oap/server/buildtools/oal/OALClassExporterTest.java b/build-tools/oal-exporter/src/test/java/org/apache/skywalking/oap/server/buildtools/oal/OALClassExporterTest.java new file mode 100644 index 0000000..27a1866 --- /dev/null +++ b/build-tools/oal-exporter/src/test/java/org/apache/skywalking/oap/server/buildtools/oal/OALClassExporterTest.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.skywalking.oap.server.buildtools.oal; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.skywalking.oap.server.core.oal.rt.OALDefine; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class OALClassExporterTest { + + /** + * All expected OAL script files. If a new OALDefine is added upstream, + * this test will fail — reminding us to update ALL_DEFINES. + */ + private static final Set<String> EXPECTED_OAL_SCRIPTS = Set.of( + "oal/disable.oal", + "oal/core.oal", + "oal/java-agent.oal", + "oal/dotnet-agent.oal", + "oal/browser.oal", + "oal/mesh.oal", + "oal/ebpf.oal", + "oal/tcp.oal", + "oal/cilium.oal" + ); + + @Test + void allDefinesShouldCoverExpectedOALScripts() { + Set<String> actualScripts = Arrays.stream(OALClassExporter.ALL_DEFINES) + .map(OALDefine::getConfigFile) + .collect(Collectors.toSet()); + + assertEquals( + EXPECTED_OAL_SCRIPTS, actualScripts, + "ALL_DEFINES does not match expected OAL scripts" + ); + } + + @Test + void allOALScriptsShouldBeOnClasspath() { + ClassLoader cl = getClass().getClassLoader(); + for (OALDefine define : OALClassExporter.ALL_DEFINES) { + assertNotNull( + cl.getResource(define.getConfigFile()), + "OAL script not found on classpath: " + define.getConfigFile() + + " (" + define.getClass().getSimpleName() + ")" + ); + } + } + + @Test + void exportShouldGenerateClassesForEachNonDisableDefine(@TempDir Path tempDir) + throws Exception { + String outputDir = tempDir.toString(); + + // Run the full export + OALClassExporter.main(new String[] {outputDir}); + + // Verify metrics classes generated + Path metricsDir = tempDir.resolve( + "org/apache/skywalking/oap/server/core/source/oal/rt/metrics"); + assertTrue(Files.isDirectory(metricsDir), "metrics directory should exist"); + + long metricsCount = Files.list(metricsDir) + .filter(p -> p.toString().endsWith(".class")) + .count(); + assertTrue(metricsCount > 0, "Should generate metrics classes, got 0"); + + // Verify dispatcher classes generated + Path dispatcherDir = tempDir.resolve( + "org/apache/skywalking/oap/server/core/source/oal/rt/dispatcher"); + assertTrue(Files.isDirectory(dispatcherDir), "dispatcher directory should exist"); + + long dispatcherCount = Files.list(dispatcherDir) + .filter(p -> p.toString().endsWith(".class")) + .count(); + assertTrue(dispatcherCount > 0, "Should generate dispatcher classes, got 0"); + + // Verify builder classes generated + Path builderDir = tempDir.resolve( + "org/apache/skywalking/oap/server/core/source/oal/rt/metrics/builder"); + assertTrue(Files.isDirectory(builderDir), "builder directory should exist"); + + long builderCount = Files.list(builderDir) + .filter(p -> p.toString().endsWith(".class")) + .count(); + assertTrue(builderCount > 0, "Should generate builder classes, got 0"); + + // Verify manifest files + Path metricsManifest = tempDir.resolve("META-INF/oal-metrics-classes.txt"); + assertTrue(Files.exists(metricsManifest), "metrics manifest should exist"); + List<String> metricsLines = Files.readAllLines(metricsManifest); + assertFalse(metricsLines.isEmpty(), "metrics manifest should not be empty"); + + Path dispatcherManifest = tempDir.resolve("META-INF/oal-dispatcher-classes.txt"); + assertTrue(Files.exists(dispatcherManifest), "dispatcher manifest should exist"); + List<String> dispatcherLines = Files.readAllLines(dispatcherManifest); + assertFalse(dispatcherLines.isEmpty(), "dispatcher manifest should not be empty"); + + // Verify disabled sources manifest exists (may be empty when all disable() calls are commented out) + Path disabledManifest = tempDir.resolve("META-INF/oal-disabled-sources.txt"); + assertTrue(Files.exists(disabledManifest), "disabled sources manifest should exist"); + } +}
