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 0a40450918b24206a3e9c51a71f7c565f5cc84c1 Author: Wu Sheng <[email protected]> AuthorDate: Wed Feb 18 22:04:58 2026 +0800 Add MAL-IMMIGRATION.md: plan for MAL/LAL build-time pre-compilation Detailed technical document covering 3 GraalVM-incompatible patterns: 1. MeterSystem ClassPath scanning (16 @MeterFunction classes) 2. MeterSystem Javassist dynamic class generation (~1188 rules) 3. Groovy runtime compilation (MAL dynamic + LAL @CompileStatic) Solution structured in 3 steps: - Step 1: MeterFunction manifest + same-FQCN MeterSystem replacement - Step 2: Build-time MAL/LAL pre-compilation + MeterSystem class generation via build-tools/mal-compiler - Step 3: Verification tests Key finding: MAL cannot use @CompileStatic due to propertyMissing(), ExpandoMetaClass on Number, and dynamic closures. Pre-compilation uses standard dynamic Groovy; native image compatibility is a Phase 3 concern. Co-Authored-By: Claude Opus 4.6 <[email protected]> --- MAL-IMMIGRATION.md | 444 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 444 insertions(+) diff --git a/MAL-IMMIGRATION.md b/MAL-IMMIGRATION.md new file mode 100644 index 0000000..4121900 --- /dev/null +++ b/MAL-IMMIGRATION.md @@ -0,0 +1,444 @@ +# Phase 2: MAL/LAL Build-Time Pre-Compilation + +## Context + +MAL (Meter Analysis Language) and LAL (Log Analysis Language) have three GraalVM-incompatible runtime patterns: + +1. **MeterSystem ClassPath scanning**: `MeterSystem` constructor uses Guava `ClassPath.from()` to discover `@MeterFunction`-annotated classes (16 meter functions). +2. **MeterSystem Javassist dynamic class generation**: `MeterSystem.create()` uses Javassist `ClassPool.makeClass()` to create one dynamic meter subclass per metric rule at runtime (~1188 rules across all MAL sources). Classes live in `org.apache.skywalking.oap.server.core.analysis.meter.dynamic.*`. +3. **Groovy runtime compilation**: MAL uses `GroovyShell.parse()` with dynamic Groovy features. LAL uses `GroovyShell.parse()` with `@CompileStatic`. Both compile scripts at startup. + +**Key architectural difference from OAL**: MAL initialization is a tightly coupled pipeline where Groovy compilation, static analysis, and Javassist generation are interleaved: + +``` +Rule YAML → MetricConvert constructor → Analyzer.build() + → DSL.parse() [Groovy compilation] + → e.parse() [static analysis → scopeType, functionName, metricType] + → meterSystem.create() [Javassist class generation + MetricsStreamProcessor registration] +``` + +The build tool must execute this entire chain — Groovy pre-compilation and Javassist pre-generation cannot be separated. + +**Solution**: Run the full MAL/LAL initialization at build time. Export Javassist-generated `.class` files + compiled Groovy script bytecode. At runtime, load pre-generated classes from manifests — no ClassPath scanning, no Javassist, no Groovy compilation. + +--- + +## Rule file inventory + +| Source | Path | Loader | Files | Metric Rules | +|--------|------|--------|-------|--------------| +| Agent meter | `meter-analyzer-config/` | `MeterConfigs.loadConfig()` | 11 | ~147 | +| OTel metrics | `otel-rules/` | `Rules.loadRules()` | 55 | ~1039 | +| Log MAL | `log-mal-rules/` | `Rules.loadRules()` | 2 | ~2 | +| LAL scripts | `lal/` | `LALConfigs.load()` | 8 | 10 | +| **Total** | | | **76** | **~1198** | + +Each MAL metric rule generates one Groovy script + one Javassist dynamic meter class. Each LAL rule generates one Groovy script. + +--- + +## Step 1: MeterFunction Manifest + Same-FQCN MeterSystem + +### Problem + +`MeterSystem` constructor (`MeterSystem.java:75-96`) scans the classpath: + +```java +ClassPath classpath = ClassPath.from(MeterSystem.class.getClassLoader()); +ImmutableSet<ClassPath.ClassInfo> classes = classpath.getTopLevelClassesRecursive("org.apache.skywalking"); +for (ClassPath.ClassInfo classInfo : classes) { + Class<?> functionClass = classInfo.load(); + if (functionClass.isAnnotationPresent(MeterFunction.class)) { + functionRegister.put(metricsFunction.functionName(), functionClass); + } +} +``` + +### 16 Meter Function classes + +| Function Name | Class | Accept Type | +|---|---|---| +| `avg` | `AvgFunction` | `Long` | +| `avgLabeled` | `AvgLabeledFunction` | `DataTable` | +| `avgHistogram` | `AvgHistogramFunction` | `BucketedValues` | +| `avgHistogramPercentile` | `AvgHistogramPercentileFunction` | `PercentileArgument` | +| `latest` | `LatestFunction` | `Long` | +| `latestLabeled` | `LatestLabeledFunction` | `DataTable` | +| `max` | `MaxFunction` | `Long` | +| `maxLabeled` | `MaxLabeledFunction` | `DataTable` | +| `min` | `MinFunction` | `Long` | +| `minLabeled` | `MinLabeledFunction` | `DataTable` | +| `sum` | `SumFunction` | `Long` | +| `sumLabeled` | `SumLabeledFunction` | `DataTable` | +| `sumHistogram` | `HistogramFunction` | `BucketedValues` | +| `sumHistogramPercentile` | `SumHistogramPercentileFunction` | `PercentileArgument` | +| `sumPerMin` | `SumPerMinFunction` | `Long` | +| `sumPerMinLabeled` | `SumPerMinLabeledFunction` | `DataTable` | + +### Approach + +1. Extend `OALClassExporter` to scan `@MeterFunction` annotation at build time. Write `META-INF/annotation-scan/MeterFunction.txt` with format `functionName=FQCN` (one entry per line). + +2. Create same-FQCN replacement `MeterSystem` in `oap-graalvm-server` that reads function registry from the manifest instead of `ClassPath.from()`: + +```java +// Pseudocode for replacement MeterSystem constructor +public MeterSystem(ModuleManager manager) { + this.manager = manager; + this.classPool = ClassPool.getDefault(); + + // Read from manifest instead of ClassPath.from() + for (String line : readManifest("META-INF/annotation-scan/MeterFunction.txt")) { + String[] parts = line.split("=", 2); + String functionName = parts[0]; + Class<?> functionClass = Class.forName(parts[1]); + functionRegister.put(functionName, (Class<? extends AcceptableValue>) functionClass); + } +} +``` + +At this step, `create()` still uses Javassist — this is fine for JVM mode. Step 2 eliminates it. + +--- + +## Step 2: Build-Time MAL/LAL Pre-Compilation + MeterSystem Class Generation + +**Module**: `build-tools/mal-compiler` (skeleton exists) + +### MALCompiler.java — main build tool + +The tool executes the full initialization pipeline at build time: + +#### Phase A: Initialize infrastructure + +```java +// 1. Initialize scope registry (same as OALClassExporter) +DefaultScopeDefine.reset(); +AnnotationScan scopeScan = new AnnotationScan(); +scopeScan.registerListener(new DefaultScopeDefine.Listener()); +scopeScan.scan(); + +// 2. Create ExportingMeterSystem — intercepts Javassist to export .class files +ExportingMeterSystem meterSystem = new ExportingMeterSystem(outputDir); +``` + +#### Phase B: Load and compile all MAL rules + +```java +// 3. Agent meter rules (meter-analyzer-config/) +List<MeterConfig> agentConfigs = MeterConfigs.loadConfig("meter-analyzer-config", activeFiles); +for (MeterConfig config : agentConfigs) { + new MetricConvert(config, meterSystem); // triggers: Groovy compile + parse + Javassist +} + +// 4. OTel rules (otel-rules/) +List<Rule> otelRules = Rules.loadRules("otel-rules", enabledOtelRules); +for (Rule rule : otelRules) { + new MetricConvert(rule, meterSystem); +} + +// 5. Log MAL rules (log-mal-rules/) +List<Rule> logMalRules = Rules.loadRules("log-mal-rules", enabledLogMalRules); +for (Rule rule : logMalRules) { + new MetricConvert(rule, meterSystem); +} +``` + +#### Phase C: Load and compile all LAL rules + +```java +// 6. LAL rules (lal/) +List<LALConfigs> lalConfigs = LALConfigs.load("lal", lalFiles); +for (LALConfig config : flattenedConfigs) { + DSL.of(moduleManager, logConfig, config.getDsl()); // Groovy compile with @CompileStatic +} +``` + +#### Phase D: Export bytecode + write manifests + +The tool intercepts both Groovy and Javassist class generation to capture bytecode: + +**Javassist interception** (`ExportingMeterSystem`): Override `create()` to call `ctClass.toBytecode()` and write `.class` files to the output directory. Also records metadata for the manifest. + +**Groovy script capture**: Use Groovy's `CompilationUnit` API or intercept `GroovyShell.parse()` to extract compiled script bytecode. Each MAL expression compiles to a unique script class (name based on hash or sequential index). + +**Manifest files**: + +`META-INF/mal-meter-classes.txt` — Javassist-generated meter classes: +``` +# format: metricsName|scopeId|functionName|dataType|FQCN +meter_java_agent_created_tracing_context_count|1|sum|java.lang.Long|org.apache.skywalking.oap.server.core.analysis.meter.dynamic.meter_java_agent_created_tracing_context_count +... +``` + +`META-INF/mal-groovy-scripts.txt` — Pre-compiled MAL Groovy scripts: +``` +# format: metricName=scriptClassName +meter_java_agent_created_tracing_context_count=org.apache.skywalking.oap.server.core.analysis.meter.script.Script0001 +... +``` + +`META-INF/lal-scripts.txt` — Pre-compiled LAL Groovy scripts: +``` +# format: layer:ruleName=scriptClassName +GENERAL:default=org.apache.skywalking.oap.server.core.analysis.lal.script.LALScript0001 +NGINX:nginx-access-log=org.apache.skywalking.oap.server.core.analysis.lal.script.LALScript0002 +... +``` + +### ExportingMeterSystem — Javassist interception + +This is a build-time variant of `MeterSystem` that exports bytecode instead of loading classes: + +```java +// Pseudocode +public class ExportingMeterSystem extends MeterSystem { + private final Path outputDir; + private final List<MeterClassMetadata> metadata = new ArrayList<>(); + + @Override + public synchronized <T> void create(String metricsName, String functionName, + ScopeType type, Class<T> dataType) { + // Same Javassist logic as upstream MeterSystem.create() + CtClass metricsClass = classPool.makeClass(METER_CLASS_PACKAGE + className, parentClass); + // ... add constructor and createNew() method ... + + // Instead of toClass(), write bytecode to disk + byte[] bytecode = metricsClass.toBytecode(); + Path classFile = outputDir.resolve(packageToPath(METER_CLASS_PACKAGE + className) + ".class"); + Files.createDirectories(classFile.getParent()); + Files.write(classFile, bytecode); + + // Record metadata for manifest + metadata.add(new MeterClassMetadata(metricsName, type.getScopeId(), + functionName, dataType.getName(), METER_CLASS_PACKAGE + className)); + } +} +``` + +### Same-FQCN replacement classes + +**1. MAL `DSL`** (`oap-graalvm-server/.../meter/analyzer/dsl/DSL.java`) + +Same FQCN as `org.apache.skywalking.oap.meter.analyzer.dsl.DSL`. `parse()` loads a pre-compiled Groovy script class instead of calling `GroovyShell.parse()`: + +```java +// Pseudocode +public static Expression parse(String metricName, String expression) { + // Look up pre-compiled script class from manifest + String scriptClassName = lookupScript(metricName); + Class<?> scriptClass = Class.forName(scriptClassName); + DelegatingScript script = (DelegatingScript) scriptClass.getDeclaredConstructor().newInstance(); + + // Same CompilerConfiguration setup (imports, security) is baked into the pre-compiled class + return new Expression(metricName, expression, script); +} +``` + +The `Expression.empower()` method still runs at runtime — it calls `setDelegate()` and `ExpandoMetaClass` registration. These are Java API calls on the Groovy runtime, not compilation. + +**2. `FilterExpression`** (`oap-graalvm-server/.../meter/analyzer/dsl/FilterExpression.java`) + +Same FQCN. Loads pre-compiled filter closure class instead of `GroovyShell.evaluate()`. + +**3. LAL `DSL`** (`oap-graalvm-server/.../log/analyzer/dsl/DSL.java`) + +Same FQCN as `org.apache.skywalking.oap.log.analyzer.dsl.DSL`. `of()` loads pre-compiled LAL script class (already `@CompileStatic`) instead of calling `GroovyShell.parse()`: + +```java +// Pseudocode +public static DSL of(ModuleManager moduleManager, LogAnalyzerModuleConfig config, String dsl) { + String scriptClassName = lookupLALScript(layer, ruleName); + Class<?> scriptClass = Class.forName(scriptClassName); + DelegatingScript script = (DelegatingScript) scriptClass.getDeclaredConstructor().newInstance(); + FilterSpec filterSpec = new FilterSpec(moduleManager, config); + script.setDelegate(filterSpec); + return new DSL(script, filterSpec); +} +``` + +**4. `MeterSystem`** (enhanced from Step 1) + +The `create()` method becomes a manifest lookup + `MetricsStreamProcessor` registration: + +```java +// Pseudocode +public synchronized <T> void create(String metricsName, String functionName, + ScopeType type, Class<T> dataType) { + if (meterPrototypes.containsKey(metricsName)) { + return; // already registered + } + + // Load pre-generated class from classpath + MeterClassMetadata meta = lookupMeterClass(metricsName); + Class<?> targetClass = Class.forName(meta.fqcn); + AcceptableValue prototype = (AcceptableValue) targetClass.getDeclaredConstructor().newInstance(); + meterPrototypes.put(metricsName, new MeterDefinition(type, prototype, dataType)); + + // Register with stream processor (same as upstream) + MetricsStreamProcessor.getInstance().create( + manager, + new StreamDefinition(metricsName, type.getScopeId(), prototype.builder(), + MetricsStreamProcessor.class), + targetClass + ); +} +``` + +`buildMetrics()` and `doStreamingCalculation()` work unchanged — they use the prototype map. + +### MAL Groovy: Why @CompileStatic is NOT possible + +MAL expressions rely on three dynamic Groovy features: + +1. **`propertyMissing(String)`** (`Expression.java:126`): When an expression references `counter` or `jvm_memory_bytes_used`, Groovy calls `ExpressionDelegate.propertyMissing(sampleName)` which looks up the sample family from a `ThreadLocal<Map<String, SampleFamily>>`. Static compilation cannot resolve these properties. + +2. **`ExpandoMetaClass` on `Number`** (`Expression.java:104-111`): The `empower()` method registers `plus`, `minus`, `multiply`, `div` closures on `Number.class` to allow expressions like `100 * server_cpu_seconds`. This is runtime metaclass manipulation. + +3. **Closure arguments** in DSL methods: Expressions like `.tag({tags -> tags.gc = 'young_gc'})` and `.filter({tags -> tags.job_name == 'mysql'})` pass Groovy closures with dynamic property access. + +**Approach**: Pre-compile using standard dynamic Groovy (same `CompilerConfiguration` as upstream — `DelegatingScript` base class, `SecureASTCustomizer`, `ImportCustomizer`). The compiled `.class` files contain the same bytecode that `GroovyShell.parse()` would produce. At runtime, `Class.forName()` + `newInstance()` loads the pre-compiled script, and `Expression.empower()` sets up the delegate and `ExpandoMetaClass`. + +**GraalVM native image risk** (Phase 3 concern): Dynamic Groovy compiled classes use `invokedynamic` and Groovy's MOP (Meta-Object Protocol) for method resolution. In native image, this may need: +- `reflect-config.json` for all compiled script classes +- Groovy MOP runtime classes registered for reachability +- If `ExpandoMetaClass` does not work in native image: fallback to upstream DSL changes (replace operator overloading with explicit `SampleFamily.multiply(100, sf)` calls) + +Phase 2 focuses on JVM mode — pre-compilation for startup speed. Phase 3 addresses native image compatibility. + +### LAL Groovy: Already @CompileStatic + +LAL already uses `@CompileStatic` with `LALPrecompiledExtension` for type checking. The `CompilationUnit` approach works directly. Compiled scripts extend `LALDelegatingScript` and are fully statically typed. + +--- + +## Step 3: Verification Tests + +### MALCompilerTest (in `build-tools/mal-compiler/src/test/`) + +``` +- allRuleYamlFilesLoadable: + Verify all 76 rule files exist on classpath + +- fullCompilationGeneratesExpectedCounts: + Run MALCompiler.main(), verify: + - Generated meter .class count > 0 for each rule source + - Groovy script .class count matches rule count + - All 3 manifest files exist and are non-empty + +- manifestEntriesAreWellFormed: + Parse manifest files, verify format and field count per line +``` + +### MALPrecompiledRegistrationTest (in `oap-graalvm-server/src/test/`) + +``` +- meterFunctionManifestMatchesClasspath: + Compare manifest against Guava ClassPath scan of @MeterFunction + (same pattern as PrecompiledRegistrationTest for OAL) + +- all16MeterFunctionsInManifest: + Verify all 16 known function names appear + +- precompiledMeterClassesLoadable: + For each entry in mal-meter-classes.txt: + - Class.forName() succeeds + - Class extends the correct meter function parent + +- precompiledGroovyScriptsLoadable: + For each entry in mal-groovy-scripts.txt: + - Class.forName() succeeds + - Class is assignable to DelegatingScript + +- precompiledLALScriptsLoadable: + For each entry in lal-scripts.txt: + - Class.forName() succeeds + - Class is assignable to LALDelegatingScript +``` + +--- + +## Files to Create + +1. **`build-tools/mal-compiler/src/main/java/.../buildtools/mal/MALCompiler.java`** + - Main build tool: loads all MAL/LAL rules, runs full initialization pipeline, exports .class files + manifests + +2. **`build-tools/mal-compiler/src/main/java/.../buildtools/mal/ExportingMeterSystem.java`** + - MeterSystem variant that exports Javassist bytecode to disk via `toBytecode()` instead of `toClass()` + +3. **`oap-graalvm-server/src/main/java/.../core/analysis/meter/MeterSystem.java`** + - Same-FQCN replacement: reads function registry from manifest, loads pre-generated classes in `create()` + +4. **`oap-graalvm-server/src/main/java/.../meter/analyzer/dsl/DSL.java`** + - Same-FQCN replacement: loads pre-compiled MAL Groovy scripts from manifest + +5. **`oap-graalvm-server/src/main/java/.../meter/analyzer/dsl/FilterExpression.java`** + - Same-FQCN replacement: loads pre-compiled filter closures from manifest + +6. **`oap-graalvm-server/src/main/java/.../log/analyzer/dsl/DSL.java`** + - Same-FQCN replacement: loads pre-compiled LAL scripts from manifest + +7. **`build-tools/mal-compiler/src/test/java/.../buildtools/mal/MALCompilerTest.java`** + - Build-time compilation tests + +8. **`oap-graalvm-server/src/test/java/.../graalvm/MALPrecompiledRegistrationTest.java`** + - Runtime registration and loading tests + +## Files to Modify + +1. **`build-tools/oal-exporter/src/main/java/.../buildtools/oal/OALClassExporter.java`** + - Add `@MeterFunction` annotation scan → `META-INF/annotation-scan/MeterFunction.txt` + +2. **`build-tools/mal-compiler/pom.xml`** + - Add dependencies (server-core, meter-analyzer, log-analyzer, agent-analyzer, all receiver plugins for rule YAMLs) + - Configure `exec-maven-plugin` + `maven-jar-plugin` + +3. **`oap-graalvm-server/pom.xml`** + - Add dependency on `mal-compiler` generated JAR + +4. **`build-tools/oal-exporter/src/test/java/.../OALClassExporterTest.java`** + - Add test for MeterFunction manifest + +## Key Upstream Files (read-only) + +- `MeterSystem.java` — ClassPath scan (`constructor:75-96`) + Javassist class gen (`create():180-259`) +- `DSL.java` (meter-analyzer) — MAL Groovy compilation: `GroovyShell.parse()` with `DelegatingScript`, `SecureASTCustomizer` +- `Expression.java` — Script execution: `DelegatingScript.run()`, `ExpandoMetaClass` on Number, `ExpressionDelegate.propertyMissing()` +- `FilterExpression.java` — Filter closure compilation: `GroovyShell.evaluate()` +- `Analyzer.java` — Initialization chain: `build()` → `DSL.parse()` → `e.parse()` → `meterSystem.create()` +- `MetricConvert.java` — Rule → Analyzer creation: `new MetricConvert(rule, meterSystem)` triggers full pipeline +- `DSL.java` (log-analyzer) — LAL Groovy compilation: `@CompileStatic` + `LALPrecompiledExtension` +- `LALDelegatingScript.java` — LAL script base class: `filter()`, `json()`, `text()`, `extractor()`, `sink()` +- `LogFilterListener.java` — LAL DSL factory: `DSL.of()` for each rule, stores in `Map<Layer, Map<String, DSL>>` +- `Rules.java` — OTel/log-MAL rule loading: `loadRules(path, enabledRules)` +- `MeterConfigs.java` — Agent meter rule loading: `loadConfig(path, fileNames)` +- `LALConfigs.java` — LAL rule loading: `load(path, files)` +- `MeterFunction.java` — Annotation: `@MeterFunction(functionName = "...")` on meter function classes +- `AcceptableValue.java` — Interface all meter functions implement +- `MeterClassPackageHolder.java` — Package anchor for Javassist-generated classes + +--- + +## Verification + +```bash +# 1. Build everything +make build-distro + +# 2. Check generated meter classes exist +ls build-tools/mal-compiler/target/generated-mal-classes/org/apache/skywalking/oap/server/core/analysis/meter/dynamic/ + +# 3. Check Groovy script classes exist +ls build-tools/mal-compiler/target/generated-mal-classes/org/apache/skywalking/oap/server/core/analysis/meter/script/ + +# 4. Check manifest files +cat build-tools/mal-compiler/target/generated-mal-classes/META-INF/mal-meter-classes.txt | wc -l +cat build-tools/mal-compiler/target/generated-mal-classes/META-INF/mal-groovy-scripts.txt | wc -l +cat build-tools/mal-compiler/target/generated-mal-classes/META-INF/lal-scripts.txt | wc -l + +# 5. Check MeterFunction manifest (produced by oal-exporter) +cat build-tools/oal-exporter/target/generated-oal-classes/META-INF/annotation-scan/MeterFunction.txt + +# 6. Verify tests pass +make build-distro # runs MALCompilerTest + MALPrecompiledRegistrationTest +```
