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 9a4c809699d35130f33ea0714cab253c601cf8ad Author: Wu Sheng <[email protected]> AuthorDate: Wed Feb 18 22:03:57 2026 +0800 Add OAL immigration: same-FQCN replacement classes + annotation manifests Replace 3 upstream classes via Maven classpath precedence to eliminate runtime Javassist code generation and Guava ClassPath scanning: - OALEngineLoaderService: loads pre-compiled OAL classes from build-time manifests instead of running the OAL engine at startup - AnnotationScan: reads annotation manifests from META-INF/annotation-scan/ instead of Guava ClassPath.from() scanning - SourceReceiverImpl: reads dispatcher/decorator manifests instead of Guava classpath scanning OALClassExporter enhanced to scan 6 annotation/interface types at build time (ScopeDeclaration, Stream, Disable, MultipleDisable, SourceDispatcher, ISourceDecorator) and write manifests. Verification: OALClassExporterTest (3 tests) + PrecompiledRegistrationTest (12 tests) covering manifest-vs-classpath comparison, class loading, scope registration, and source→dispatcher→metrics chain consistency. Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../server/buildtools/oal/OALClassExporter.java | 87 ++++- .../buildtools/oal/OALClassExporterTest.java | 26 ++ oap-graalvm-server/pom.xml | 15 + .../oap/server/core/annotation/AnnotationScan.java | 125 +++++++ .../server/core/oal/rt/OALEngineLoaderService.java | 129 +++++++ .../oap/server/core/source/SourceReceiverImpl.java | 112 ++++++ .../graalvm/PrecompiledRegistrationTest.java | 392 +++++++++++++++++++++ 7 files changed, 881 insertions(+), 5 deletions(-) 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 index ded57e7..614763d 100644 --- 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 @@ -17,8 +17,12 @@ package org.apache.skywalking.oap.server.buildtools.oal; +import com.google.common.collect.ImmutableSet; +import com.google.common.reflect.ClassPath; import java.io.IOException; +import java.lang.annotation.Annotation; import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -31,12 +35,17 @@ 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.Disable; import org.apache.skywalking.oap.server.core.analysis.DisableRegister; +import org.apache.skywalking.oap.server.core.analysis.ISourceDecorator; +import org.apache.skywalking.oap.server.core.analysis.MultipleDisable; +import org.apache.skywalking.oap.server.core.analysis.SourceDispatcher; 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.source.ScopeDeclaration; 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; @@ -47,13 +56,11 @@ 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. + * exports generated .class files and manifest files, and scans the classpath for + * hardcoded annotated classes and interface implementations used at runtime. * * 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 { @@ -129,8 +136,31 @@ public class OALClassExporter { 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", + log.info("OAL Class Exporter: {} metrics, {} dispatchers, {} disabled sources", metricsClasses.size(), dispatcherClasses.size(), disabledSources.size()); + + // ---- Annotation & interface scanning for hardcoded classes ---- + Path annotationScanDir = metaInf.resolve("annotation-scan"); + Files.createDirectories(annotationScanDir); + + ImmutableSet<ClassPath.ClassInfo> allClasses = ClassPath + .from(OALClassExporter.class.getClassLoader()) + .getTopLevelClassesRecursive("org.apache.skywalking"); + + writeManifest(annotationScanDir.resolve("ScopeDeclaration.txt"), + scanAnnotation(allClasses, ScopeDeclaration.class)); + writeManifest(annotationScanDir.resolve("Stream.txt"), + scanAnnotation(allClasses, org.apache.skywalking.oap.server.core.analysis.Stream.class)); + writeManifest(annotationScanDir.resolve("Disable.txt"), + scanAnnotation(allClasses, Disable.class)); + writeManifest(annotationScanDir.resolve("MultipleDisable.txt"), + scanAnnotation(allClasses, MultipleDisable.class)); + writeManifest(annotationScanDir.resolve("SourceDispatcher.txt"), + scanInterface(allClasses, SourceDispatcher.class)); + writeManifest(annotationScanDir.resolve("ISourceDecorator.txt"), + scanInterface(allClasses, ISourceDecorator.class)); + + log.info("OAL Class Exporter: done"); } /** @@ -195,6 +225,53 @@ public class OALClassExporter { return result; } + /** + * Scan for classes annotated with the given annotation type. + */ + private static List<String> scanAnnotation( + ImmutableSet<ClassPath.ClassInfo> allClasses, + Class<? extends Annotation> annotationType) { + + List<String> result = new ArrayList<>(); + for (ClassPath.ClassInfo classInfo : allClasses) { + try { + Class<?> aClass = classInfo.load(); + if (aClass.isAnnotationPresent(annotationType)) { + result.add(aClass.getName()); + } + } catch (NoClassDefFoundError | Exception ignored) { + // Some classes may fail to load due to missing optional dependencies + } + } + Collections.sort(result); + log.info("Scanned @{}: {} classes", annotationType.getSimpleName(), result.size()); + return result; + } + + /** + * Scan for concrete classes implementing the given interface. + */ + private static List<String> scanInterface( + ImmutableSet<ClassPath.ClassInfo> allClasses, Class<?> interfaceType) { + + List<String> result = new ArrayList<>(); + for (ClassPath.ClassInfo classInfo : allClasses) { + try { + Class<?> aClass = classInfo.load(); + if (!aClass.isInterface() + && !Modifier.isAbstract(aClass.getModifiers()) + && interfaceType.isAssignableFrom(aClass)) { + result.add(aClass.getName()); + } + } catch (NoClassDefFoundError | Exception ignored) { + // Some classes may fail to load due to missing optional dependencies + } + } + Collections.sort(result); + log.info("Scanned {}: {} classes", interfaceType.getSimpleName(), result.size()); + 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 index 27a1866..8384d51 100644 --- 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 @@ -126,5 +126,31 @@ class OALClassExporterTest { // 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"); + + // Verify annotation scan manifests + Path annotationScanDir = tempDir.resolve("META-INF/annotation-scan"); + assertTrue(Files.isDirectory(annotationScanDir), "annotation-scan directory should exist"); + + assertManifestNotEmpty(annotationScanDir, "ScopeDeclaration.txt"); + assertManifestNotEmpty(annotationScanDir, "Stream.txt"); + assertManifestNotEmpty(annotationScanDir, "SourceDispatcher.txt"); + assertManifestNotEmpty(annotationScanDir, "ISourceDecorator.txt"); + + // Disable/MultipleDisable manifests exist but may be empty (no classes use these annotations currently) + assertTrue( + Files.exists(annotationScanDir.resolve("Disable.txt")), + "Disable manifest should exist" + ); + assertTrue( + Files.exists(annotationScanDir.resolve("MultipleDisable.txt")), + "MultipleDisable manifest should exist" + ); + } + + private void assertManifestNotEmpty(Path dir, String fileName) throws Exception { + Path manifest = dir.resolve(fileName); + assertTrue(Files.exists(manifest), fileName + " should exist"); + List<String> lines = Files.readAllLines(manifest); + assertFalse(lines.isEmpty(), fileName + " should not be empty"); } } diff --git a/oap-graalvm-server/pom.xml b/oap-graalvm-server/pom.xml index dfc7881..37dec83 100644 --- a/oap-graalvm-server/pom.xml +++ b/oap-graalvm-server/pom.xml @@ -241,10 +241,25 @@ <artifactId>server-starter</artifactId> </dependency> + <!-- Pre-compiled OAL classes + annotation manifests from build-time export --> + <dependency> + <groupId>org.apache.skywalking</groupId> + <artifactId>oal-exporter</artifactId> + <version>${project.version}</version> + <classifier>generated</classifier> + </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> </project> \ No newline at end of file diff --git a/oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/core/annotation/AnnotationScan.java b/oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/core/annotation/AnnotationScan.java new file mode 100644 index 0000000..9ec96fa --- /dev/null +++ b/oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/core/annotation/AnnotationScan.java @@ -0,0 +1,125 @@ +/* + * 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.core.annotation; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.annotation.Annotation; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.skywalking.oap.server.core.storage.StorageException; + +/** + * GraalVM replacement for upstream AnnotationScan. + * Same FQCN — shadows the upstream class via Maven classpath precedence. + * + * Instead of Guava ClassPath scanning, reads pre-built manifest files + * produced by OALClassExporter at build time. + * Manifest path: META-INF/annotation-scan/{AnnotationSimpleName}.txt + */ +@Slf4j +public class AnnotationScan { + + private static final String MANIFEST_PREFIX = "META-INF/annotation-scan/"; + + private final List<AnnotationListenerCache> listeners; + + public AnnotationScan() { + this.listeners = new LinkedList<>(); + } + + public void registerListener(AnnotationListener listener) { + listeners.add(new AnnotationListenerCache(listener)); + } + + public void scan() throws IOException, StorageException { + for (AnnotationListenerCache listener : listeners) { + String annotationName = listener.annotation().getSimpleName(); + String manifestPath = MANIFEST_PREFIX + annotationName + ".txt"; + + List<String> classNames = readManifest(manifestPath); + for (String className : classNames) { + try { + Class<?> aClass = Class.forName(className); + if (aClass.isAnnotationPresent(listener.annotation())) { + listener.addMatch(aClass); + } + } catch (ClassNotFoundException e) { + log.warn("Class not found from manifest {}: {}", manifestPath, className); + } + } + } + + for (AnnotationListenerCache listener : listeners) { + listener.complete(); + } + } + + private static List<String> readManifest(String resourcePath) throws IOException { + List<String> lines = new ArrayList<>(); + ClassLoader cl = AnnotationScan.class.getClassLoader(); + try (InputStream is = cl.getResourceAsStream(resourcePath)) { + if (is == null) { + log.warn("Annotation manifest not found: {}", resourcePath); + return lines; + } + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(is, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty()) { + lines.add(line); + } + } + } + } + return lines; + } + + private static class AnnotationListenerCache { + private final AnnotationListener listener; + private final List<Class<?>> matchedClass; + + private AnnotationListenerCache(AnnotationListener listener) { + this.listener = listener; + this.matchedClass = new LinkedList<>(); + } + + private Class<? extends Annotation> annotation() { + return this.listener.annotation(); + } + + private void addMatch(Class<?> aClass) { + matchedClass.add(aClass); + } + + private void complete() throws StorageException { + matchedClass.sort(Comparator.comparing(Class::getName)); + for (Class<?> aClass : matchedClass) { + listener.notify(aClass); + } + } + } +} diff --git a/oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/core/oal/rt/OALEngineLoaderService.java b/oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/core/oal/rt/OALEngineLoaderService.java new file mode 100644 index 0000000..fa40c7b --- /dev/null +++ b/oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/core/oal/rt/OALEngineLoaderService.java @@ -0,0 +1,129 @@ +/* + * 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.core.oal.rt; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.apache.skywalking.oap.server.core.CoreModule; +import org.apache.skywalking.oap.server.core.analysis.DisableRegister; +import org.apache.skywalking.oap.server.core.analysis.StreamAnnotationListener; +import org.apache.skywalking.oap.server.core.source.SourceReceiver; +import org.apache.skywalking.oap.server.library.module.ModuleManager; +import org.apache.skywalking.oap.server.library.module.ModuleStartException; +import org.apache.skywalking.oap.server.library.module.Service; + +/** + * GraalVM replacement for upstream OALEngineLoaderService. + * Same FQCN — shadows the upstream class via Maven classpath precedence. + * + * Instead of running the OAL engine (Javassist code generation) at runtime, + * loads pre-compiled classes from build-time manifests produced by OALClassExporter. + */ +@Slf4j +public class OALEngineLoaderService implements Service { + + private static final String METRICS_MANIFEST = "META-INF/oal-metrics-classes.txt"; + private static final String DISPATCHER_MANIFEST = "META-INF/oal-dispatcher-classes.txt"; + private static final String DISABLED_MANIFEST = "META-INF/oal-disabled-sources.txt"; + + private final Set<OALDefine> oalDefineSet = new HashSet<>(); + private final ModuleManager moduleManager; + private boolean loaded; + + public OALEngineLoaderService(ModuleManager moduleManager) { + this.moduleManager = moduleManager; + } + + public void load(OALDefine define) throws ModuleStartException { + if (oalDefineSet.contains(define)) { + return; + } + + if (!loaded) { + loadAllPrecompiledClasses(); + loaded = true; + } + + oalDefineSet.add(define); + } + + private void loadAllPrecompiledClasses() throws ModuleStartException { + try { + // 1. Register disabled sources before processing metrics + List<String> disabledSources = readManifest(DISABLED_MANIFEST); + for (String name : disabledSources) { + DisableRegister.INSTANCE.add(name); + } + log.info("Loaded {} disabled sources from manifest", disabledSources.size()); + + // 2. Load and register pre-compiled metrics classes + StreamAnnotationListener streamListener = new StreamAnnotationListener(moduleManager); + List<String> metricsClassNames = readManifest(METRICS_MANIFEST); + for (String className : metricsClassNames) { + Class<?> metricsClass = Class.forName(className); + streamListener.notify(metricsClass); + } + log.info("Registered {} pre-compiled OAL metrics classes", metricsClassNames.size()); + + // 3. Load and register pre-compiled dispatcher classes + var dispatcherListener = moduleManager.find(CoreModule.NAME) + .provider() + .getService(SourceReceiver.class) + .getDispatcherDetectorListener(); + List<String> dispatcherClassNames = readManifest(DISPATCHER_MANIFEST); + for (String className : dispatcherClassNames) { + Class<?> dispatcherClass = Class.forName(className); + dispatcherListener.addIfAsSourceDispatcher(dispatcherClass); + } + log.info("Registered {} pre-compiled OAL dispatcher classes", dispatcherClassNames.size()); + + } catch (Exception e) { + throw new ModuleStartException("Failed to load pre-compiled OAL classes", e); + } + } + + private static List<String> readManifest(String resourcePath) throws IOException { + List<String> lines = new ArrayList<>(); + ClassLoader cl = OALEngineLoaderService.class.getClassLoader(); + try (InputStream is = cl.getResourceAsStream(resourcePath)) { + if (is == null) { + log.warn("Manifest not found: {}", resourcePath); + return lines; + } + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(is, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty()) { + lines.add(line); + } + } + } + } + return lines; + } +} diff --git a/oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/core/source/SourceReceiverImpl.java b/oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/core/source/SourceReceiverImpl.java new file mode 100644 index 0000000..08c7b02 --- /dev/null +++ b/oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/core/source/SourceReceiverImpl.java @@ -0,0 +1,112 @@ +/* + * 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.core.source; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.skywalking.oap.server.core.analysis.DispatcherDetectorListener; +import org.apache.skywalking.oap.server.core.analysis.DispatcherManager; +import org.apache.skywalking.oap.server.core.analysis.SourceDecoratorManager; + +/** + * GraalVM replacement for upstream SourceReceiverImpl. + * Same FQCN — shadows the upstream class via Maven classpath precedence. + * + * Instead of Guava ClassPath scanning, reads pre-built manifest files + * produced by OALClassExporter at build time. + */ +@Slf4j +public class SourceReceiverImpl implements SourceReceiver { + + private static final String DISPATCHER_MANIFEST = "META-INF/annotation-scan/SourceDispatcher.txt"; + private static final String DECORATOR_MANIFEST = "META-INF/annotation-scan/ISourceDecorator.txt"; + + @Getter + private final DispatcherManager dispatcherManager; + + @Getter + private final SourceDecoratorManager sourceDecoratorManager; + + public SourceReceiverImpl() { + this.dispatcherManager = new DispatcherManager(); + this.sourceDecoratorManager = new SourceDecoratorManager(); + } + + @Override + public void receive(ISource source) { + dispatcherManager.forward(source); + } + + @Override + public DispatcherDetectorListener getDispatcherDetectorListener() { + return getDispatcherManager(); + } + + public void scan() throws IOException, InstantiationException, IllegalAccessException { + List<String> dispatcherNames = readManifest(DISPATCHER_MANIFEST); + for (String className : dispatcherNames) { + try { + Class<?> aClass = Class.forName(className); + dispatcherManager.addIfAsSourceDispatcher(aClass); + } catch (ClassNotFoundException e) { + log.warn("Dispatcher class not found: {}", className); + } + } + log.info("Registered {} source dispatchers from manifest", dispatcherNames.size()); + + List<String> decoratorNames = readManifest(DECORATOR_MANIFEST); + for (String className : decoratorNames) { + try { + Class<?> aClass = Class.forName(className); + sourceDecoratorManager.addIfAsSourceDecorator(aClass); + } catch (ClassNotFoundException e) { + log.warn("Decorator class not found: {}", className); + } + } + log.info("Registered {} source decorators from manifest", decoratorNames.size()); + } + + private static List<String> readManifest(String resourcePath) throws IOException { + List<String> lines = new ArrayList<>(); + ClassLoader cl = SourceReceiverImpl.class.getClassLoader(); + try (InputStream is = cl.getResourceAsStream(resourcePath)) { + if (is == null) { + log.warn("Manifest not found: {}", resourcePath); + return lines; + } + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(is, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty()) { + lines.add(line); + } + } + } + } + return lines; + } +} diff --git a/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/PrecompiledRegistrationTest.java b/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/PrecompiledRegistrationTest.java new file mode 100644 index 0000000..eafa1c0 --- /dev/null +++ b/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/PrecompiledRegistrationTest.java @@ -0,0 +1,392 @@ +/* + * 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.graalvm; + +import com.google.common.collect.ImmutableSet; +import com.google.common.reflect.ClassPath; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import org.apache.skywalking.oap.server.core.analysis.ISourceDecorator; +import org.apache.skywalking.oap.server.core.analysis.SourceDispatcher; +import org.apache.skywalking.oap.server.core.analysis.Stream; +import org.apache.skywalking.oap.server.core.annotation.AnnotationScan; +import org.apache.skywalking.oap.server.core.source.DefaultScopeDefine; +import org.apache.skywalking.oap.server.core.source.ISource; +import org.apache.skywalking.oap.server.core.source.ScopeDeclaration; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +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; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Verifies that pre-compiled manifests produced by OALClassExporter are complete + * and consistent. Checks: + * 1. Manifest contents match live Guava ClassPath scan (for hardcoded classes) + * 2. All OAL-generated classes are loadable and properly annotated + * 3. DefaultScopeDefine initializes correctly from manifests + * 4. Source -> Dispatcher -> Metrics chain is consistent + */ +class PrecompiledRegistrationTest { + + private static final String OAL_RT_PACKAGE = + "org.apache.skywalking.oap.server.core.source.oal.rt."; + + @BeforeAll + static void initScopeDefine() throws Exception { + DefaultScopeDefine.reset(); + AnnotationScan scopeScan = new AnnotationScan(); + scopeScan.registerListener(new DefaultScopeDefine.Listener()); + scopeScan.scan(); + } + + @AfterAll + static void cleanUp() { + DefaultScopeDefine.reset(); + } + + // ========================================================================= + // 1. Manifest vs Guava ClassPath comparison (hardcoded classes only) + // ========================================================================= + + @Test + void scopeDeclarationManifestMatchesClasspath() throws Exception { + Set<String> manifest = readManifestAsSet("META-INF/annotation-scan/ScopeDeclaration.txt"); + Set<String> scanned = guavaScanAnnotation(ScopeDeclaration.class); + assertFalse(manifest.isEmpty(), "ScopeDeclaration manifest is empty"); + assertEquals(scanned, manifest, + "ScopeDeclaration manifest does not match classpath scan"); + } + + @Test + void hardcodedStreamManifestMatchesClasspath() throws Exception { + Set<String> manifest = readManifestAsSet("META-INF/annotation-scan/Stream.txt"); + Set<String> scanned = guavaScanAnnotation(Stream.class); + // Filter out OAL-generated classes from Guava scan + scanned.removeIf(name -> name.startsWith(OAL_RT_PACKAGE)); + assertFalse(manifest.isEmpty(), "Stream manifest is empty"); + assertEquals(scanned, manifest, + "Hardcoded @Stream manifest does not match classpath scan"); + } + + @Test + void sourceDispatcherManifestMatchesClasspath() throws Exception { + Set<String> manifest = readManifestAsSet("META-INF/annotation-scan/SourceDispatcher.txt"); + Set<String> scanned = guavaScanInterface(SourceDispatcher.class); + // Filter out OAL-generated dispatchers from Guava scan + scanned.removeIf(name -> name.startsWith(OAL_RT_PACKAGE)); + assertFalse(manifest.isEmpty(), "SourceDispatcher manifest is empty"); + assertEquals(scanned, manifest, + "SourceDispatcher manifest does not match classpath scan"); + } + + @Test + void sourceDecoratorManifestMatchesClasspath() throws Exception { + Set<String> manifest = readManifestAsSet("META-INF/annotation-scan/ISourceDecorator.txt"); + Set<String> scanned = guavaScanInterface(ISourceDecorator.class); + assertFalse(manifest.isEmpty(), "ISourceDecorator manifest is empty"); + assertEquals(scanned, manifest, + "ISourceDecorator manifest does not match classpath scan"); + } + + // ========================================================================= + // 2. OAL-generated classes: loadable and properly annotated + // ========================================================================= + + @Test + void allOalMetricsClassesLoadAndHaveStreamAnnotation() throws Exception { + List<String> classes = readManifest("META-INF/oal-metrics-classes.txt"); + assertFalse(classes.isEmpty(), "OAL metrics manifest is empty"); + for (String className : classes) { + Class<?> clazz = Class.forName(className); + assertTrue(clazz.isAnnotationPresent(Stream.class), + "OAL metrics class missing @Stream: " + className); + } + } + + @Test + void allOalDispatcherClassesLoadAndImplementInterface() throws Exception { + List<String> classes = readManifest("META-INF/oal-dispatcher-classes.txt"); + assertFalse(classes.isEmpty(), "OAL dispatcher manifest is empty"); + for (String className : classes) { + Class<?> clazz = Class.forName(className); + assertTrue(SourceDispatcher.class.isAssignableFrom(clazz), + "OAL dispatcher doesn't implement SourceDispatcher: " + className); + } + } + + // ========================================================================= + // 3. Scope registration verification + // ========================================================================= + + @Test + void allDeclaredScopesRegisteredInDefaultScopeDefine() throws Exception { + List<String> classes = readManifest("META-INF/annotation-scan/ScopeDeclaration.txt"); + for (String className : classes) { + Class<?> clazz = Class.forName(className); + ScopeDeclaration decl = clazz.getAnnotation(ScopeDeclaration.class); + assertNotNull(decl, "Missing @ScopeDeclaration on " + className); + + String registeredName = DefaultScopeDefine.nameOf(decl.id()); + assertEquals(decl.name(), registeredName, + "Scope name mismatch for id=" + decl.id() + " on " + className); + } + } + + @Test + void wellKnownScopesAreRegistered() { + // Verify essential scopes exist after manifest-based initialization + assertNotNull(DefaultScopeDefine.nameOf(DefaultScopeDefine.SERVICE)); + assertNotNull(DefaultScopeDefine.nameOf(DefaultScopeDefine.SERVICE_INSTANCE)); + assertNotNull(DefaultScopeDefine.nameOf(DefaultScopeDefine.ENDPOINT)); + } + + // ========================================================================= + // 4. Source -> Dispatcher -> Metrics chain + // ========================================================================= + + @Test + void allStreamAnnotationsReferenceValidScopes() throws Exception { + // Check hardcoded @Stream classes + List<String> hardcoded = readManifest("META-INF/annotation-scan/Stream.txt"); + for (String className : hardcoded) { + Class<?> clazz = Class.forName(className); + Stream stream = clazz.getAnnotation(Stream.class); + assertNotNull(stream, "Missing @Stream on " + className); + assertScopeExists(stream.scopeId(), className); + } + + // Check OAL-generated @Stream classes + List<String> oalMetrics = readManifest("META-INF/oal-metrics-classes.txt"); + for (String className : oalMetrics) { + Class<?> clazz = Class.forName(className); + Stream stream = clazz.getAnnotation(Stream.class); + assertNotNull(stream, "Missing @Stream on OAL class " + className); + assertScopeExists(stream.scopeId(), className); + } + } + + @Test + @SuppressWarnings("deprecation") + void hardcodedDispatchersHaveDeclaredSourceScopes() throws Exception { + List<String> dispatchers = readManifest("META-INF/annotation-scan/SourceDispatcher.txt"); + for (String className : dispatchers) { + Class<?> clazz = Class.forName(className); + Class<?> sourceType = extractSourceType(clazz); + if (sourceType == null) { + fail("Cannot extract source type from " + className); + } + ISource source = (ISource) sourceType.newInstance(); + int scopeId = source.scope(); + assertScopeExists(scopeId, + className + " (source=" + sourceType.getSimpleName() + ")"); + } + } + + @Test + @SuppressWarnings("deprecation") + void oalDispatchersHaveDeclaredSourceScopes() throws Exception { + List<String> dispatchers = readManifest("META-INF/oal-dispatcher-classes.txt"); + for (String className : dispatchers) { + Class<?> clazz = Class.forName(className); + Class<?> sourceType = extractSourceType(clazz); + if (sourceType == null) { + fail("Cannot extract source type from " + className); + } + ISource source = (ISource) sourceType.newInstance(); + int scopeId = source.scope(); + assertScopeExists(scopeId, + className + " (source=" + sourceType.getSimpleName() + ")"); + } + } + + @Test + void knownScopesHaveDispatchersAndStreamMetrics() throws Exception { + // Build scope -> dispatchers map from both hardcoded and OAL manifests + Map<Integer, List<String>> scopeDispatchers = buildScopeDispatcherMap(); + + // Build scope -> @Stream metrics map from both hardcoded and OAL manifests + Map<Integer, List<String>> scopeStreams = buildScopeStreamMap(); + + // Verify key scopes have both dispatchers and stream metrics + int[] keyScopeIds = { + DefaultScopeDefine.SERVICE, + DefaultScopeDefine.ENDPOINT, + DefaultScopeDefine.SERVICE_INSTANCE, + DefaultScopeDefine.SERVICE_RELATION, + }; + + for (int scopeId : keyScopeIds) { + String scopeName = DefaultScopeDefine.nameOf(scopeId); + + assertTrue( + scopeDispatchers.containsKey(scopeId) + && !scopeDispatchers.get(scopeId).isEmpty(), + "Scope " + scopeName + " (id=" + scopeId + ") has no dispatchers" + ); + + assertTrue( + scopeStreams.containsKey(scopeId) + && !scopeStreams.get(scopeId).isEmpty(), + "Scope " + scopeName + " (id=" + scopeId + ") has no @Stream metrics" + ); + } + } + + // ========================================================================= + // Helpers + // ========================================================================= + + private static void assertScopeExists(int scopeId, String context) { + try { + DefaultScopeDefine.nameOf(scopeId); + } catch (Exception e) { + fail("Invalid scope id=" + scopeId + " referenced by " + context); + } + } + + @SuppressWarnings("deprecation") + private Map<Integer, List<String>> buildScopeDispatcherMap() throws Exception { + Map<Integer, List<String>> map = new HashMap<>(); + + List<String> allDispatchers = new ArrayList<>(); + allDispatchers.addAll(readManifest("META-INF/annotation-scan/SourceDispatcher.txt")); + allDispatchers.addAll(readManifest("META-INF/oal-dispatcher-classes.txt")); + + for (String className : allDispatchers) { + Class<?> clazz = Class.forName(className); + Class<?> sourceType = extractSourceType(clazz); + if (sourceType != null) { + ISource source = (ISource) sourceType.newInstance(); + map.computeIfAbsent(source.scope(), k -> new ArrayList<>()).add(className); + } + } + return map; + } + + private Map<Integer, List<String>> buildScopeStreamMap() throws Exception { + Map<Integer, List<String>> map = new HashMap<>(); + + List<String> allStreams = new ArrayList<>(); + allStreams.addAll(readManifest("META-INF/annotation-scan/Stream.txt")); + allStreams.addAll(readManifest("META-INF/oal-metrics-classes.txt")); + + for (String className : allStreams) { + Class<?> clazz = Class.forName(className); + Stream stream = clazz.getAnnotation(Stream.class); + if (stream != null) { + map.computeIfAbsent(stream.scopeId(), k -> new ArrayList<>()).add(className); + } + } + return map; + } + + private static Class<?> extractSourceType(Class<?> dispatcherClass) { + for (Type genericInterface : dispatcherClass.getGenericInterfaces()) { + if (genericInterface instanceof ParameterizedType) { + ParameterizedType parameterized = (ParameterizedType) genericInterface; + if (parameterized.getRawType().getTypeName().equals( + SourceDispatcher.class.getName())) { + Type[] args = parameterized.getActualTypeArguments(); + if (args.length == 1 && args[0] instanceof Class<?>) { + return (Class<?>) args[0]; + } + } + } + } + return null; + } + + private static Set<String> guavaScanAnnotation( + Class<? extends java.lang.annotation.Annotation> annotationType) throws IOException { + ClassPath classpath = ClassPath.from( + PrecompiledRegistrationTest.class.getClassLoader()); + ImmutableSet<ClassPath.ClassInfo> classes = + classpath.getTopLevelClassesRecursive("org.apache.skywalking"); + Set<String> result = new TreeSet<>(); + for (ClassPath.ClassInfo info : classes) { + try { + Class<?> clazz = info.load(); + if (clazz.isAnnotationPresent(annotationType)) { + result.add(clazz.getName()); + } + } catch (NoClassDefFoundError | Exception ignored) { + } + } + return result; + } + + private static Set<String> guavaScanInterface(Class<?> interfaceType) throws IOException { + ClassPath classpath = ClassPath.from( + PrecompiledRegistrationTest.class.getClassLoader()); + ImmutableSet<ClassPath.ClassInfo> classes = + classpath.getTopLevelClassesRecursive("org.apache.skywalking"); + Set<String> result = new TreeSet<>(); + for (ClassPath.ClassInfo info : classes) { + try { + Class<?> clazz = info.load(); + if (!clazz.isInterface() + && !Modifier.isAbstract(clazz.getModifiers()) + && interfaceType.isAssignableFrom(clazz)) { + result.add(clazz.getName()); + } + } catch (NoClassDefFoundError | Exception ignored) { + } + } + return result; + } + + private static Set<String> readManifestAsSet(String resourcePath) throws IOException { + return new TreeSet<>(readManifest(resourcePath)); + } + + private static List<String> readManifest(String resourcePath) throws IOException { + List<String> lines = new ArrayList<>(); + ClassLoader cl = PrecompiledRegistrationTest.class.getClassLoader(); + try (InputStream is = cl.getResourceAsStream(resourcePath)) { + assertNotNull(is, "Manifest not found: " + resourcePath); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(is, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty()) { + lines.add(line); + } + } + } + } + return lines; + } +}
