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
The following commit(s) were added to refs/heads/main by this push:
new b7e4c31 Add compiling guide, pre-built Docker image doc, and track
existing docs
b7e4c31 is described below
commit b7e4c31fbb0f7237a26b22d8d872b44aa3ad6806
Author: Wu Sheng <[email protected]>
AuthorDate: Fri Feb 27 08:55:25 2026 +0800
Add compiling guide, pre-built Docker image doc, and track existing docs
- Add docs/compiling.md: step-by-step build guide from source to native
image
- Add docs/docker-image.md: GHCR pre-built image usage, tags, and run
examples
- Add Setup section to docs/menu.yml grouping compiling, Docker image, and
config
- Track existing docs/ files that were previously untracked (distro-policy,
oal/mal/lal-immigration, config-init-immigration)
---
docs/README.md | 2 +
docs/compiling.md | 169 +++++++++
docs/config-init-immigration.md | 253 +++++++++++++
docs/distro-policy.md | 322 ++++++++++++++++
docs/docker-image.md | 108 ++++++
docs/lal-immigration.md | 352 +++++++++++++++++
docs/mal-immigration.md | 817 ++++++++++++++++++++++++++++++++++++++++
docs/menu.yml | 10 +-
docs/oal-immigration.md | 162 ++++++++
9 files changed, 2193 insertions(+), 2 deletions(-)
diff --git a/docs/README.md b/docs/README.md
index eec7fff..9748460 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -104,6 +104,8 @@ and asserts identical results. Tests require actual data
flow — no vacuous emp
## Further Reading
- [Distribution Policy](distro-policy.md) — full module table, architecture
constraints, build workflow
+- [Compiling from Source](compiling.md) — build JVM distro, native image, and
Docker image step by step
+- [Pre-Built Docker Images](docker-image.md) — pull and run the CI-built
native image from GHCR
- [Configuration](configuration.md) — all available settings, environment
variables, and differences from upstream
- [OAL Pre-Compilation](oal-immigration.md) — Javassist class export,
annotation scan manifests
- [MAL Transpilation](mal-immigration.md) — Groovy-to-Java transpiler,
combination pattern, functional interfaces
diff --git a/docs/compiling.md b/docs/compiling.md
new file mode 100644
index 0000000..9777ab7
--- /dev/null
+++ b/docs/compiling.md
@@ -0,0 +1,169 @@
+# Compiling from Source
+
+This guide covers building the SkyWalking GraalVM Distro from source,
producing both the
+JVM distribution and the GraalVM native image binary.
+
+## Prerequisites
+
+- **GraalVM JDK 25** with `native-image` installed
+- **Maven 3.9+** (the Maven wrapper `./mvnw` is included)
+- **Docker** (for macOS cross-compilation and container packaging)
+- **Git** (with submodule support)
+
+Set `JAVA_HOME` to your GraalVM installation for all commands below:
+
+```bash
+export JAVA_HOME=/path/to/graalvm-jdk-25
+```
+
+## Step 1: Clone the Repository
+
+```bash
+git clone --recurse-submodules
https://github.com/apache/skywalking-graalvm-distro.git
+cd skywalking-graalvm-distro
+```
+
+If you already cloned without `--recurse-submodules`:
+
+```bash
+git submodule update --init --recursive
+```
+
+## Step 2: Install SkyWalking Submodule (First Time Only)
+
+The upstream SkyWalking artifacts must be installed into your local Maven
repository before
+the distro modules can compile against them:
+
+```bash
+make init-skywalking
+```
+
+This runs `mvn install` on the `skywalking/` submodule. The result is cached
in `~/.m2/repository`
+and only needs to be re-run when the submodule is updated.
+
+## Step 3: Build the JVM Distribution
+
+```bash
+make build-distro
+```
+
+This runs the complete build pipeline:
+
+1. Compiles and installs the repackaged `*-for-graalvm` libraries
+2. Runs the build-time precompiler (OAL class export, MAL/LAL transpilation,
reflection metadata)
+3. Packages the JVM distribution with assembly
+
+Output:
+
+```
+oap-graalvm-server/target/oap-graalvm-jvm-distro/oap-graalvm-jvm-distro/ #
exploded
+oap-graalvm-server/target/oap-graalvm-jvm-distro.tar.gz #
tarball
+```
+
+### Running the JVM Distribution Locally
+
+Start BanyanDB and boot the OAP server:
+
+```bash
+make boot
+```
+
+This starts BanyanDB via Docker Compose and launches the JVM distribution with
+`SW_STORAGE_BANYANDB_TARGETS=localhost:17912`.
+
+To stop:
+
+```bash
+make shutdown
+make docker-down
+```
+
+## Step 4: Build the Native Image
+
+After `make build-distro` (or `make compile`), build the native binary:
+
+```bash
+make native-image
+```
+
+This invokes `native-maven-plugin` with the `-Pnative` profile. The native
image build
+takes several minutes and requires significant memory (~8GB RAM recommended).
+
+Output:
+
+```
+oap-graalvm-native/target/oap-graalvm-native-*-native-dist/oap-native/ #
exploded
+oap-graalvm-native/target/oap-graalvm-native-*-native-dist.tar.gz #
tarball
+```
+
+The `oap-native/` directory contains:
+
+```
+oap-native/
+├── oap-server # ~203MB native binary
+├── config/ # application.yml, alarm-settings.yml, ui-templates,
etc.
+└── log4j2.xml # Console-only logging (no RollingFile)
+```
+
+### Cross-Compiling for Linux on macOS
+
+If you are on macOS and need a Linux native binary (e.g., for Docker):
+
+```bash
+make native-image-macos
+```
+
+This runs the `native-image` step inside a
`ghcr.io/graalvm/native-image-community:25`
+container, producing a Linux binary while reusing your host's Maven cache and
compiled classes.
+
+## Step 5: Package as Docker Image
+
+After building the native image:
+
+```bash
+make docker-native
+```
+
+This builds a Docker image `skywalking-oap-native:latest` based on
`debian:bookworm-slim`
+containing only the native binary and config files.
+
+### Running with Docker Compose
+
+```bash
+docker compose -f docker/docker-compose.yml up
+```
+
+This starts BanyanDB and the OAP native image together. Ports exposed:
+
+| Port | Service |
+|------|---------|
+| 12800 | REST API (GraphQL, health check) |
+| 11800 | gRPC (agent data collection) |
+| 17912 | BanyanDB |
+
+## Other Make Targets
+
+| Target | Description |
+|--------|-------------|
+| `make compile` | Compile and install to local repo (no tests) |
+| `make test` | Run all tests (includes MAL/LAL comparison tests) |
+| `make javadoc` | Validate javadoc correctness |
+| `make dist` | Build JVM distro and print output path |
+| `make native-dist` | Build native image and print output path |
+| `make clean` | Clean all build artifacts |
+| `make docker-up` | Start BanyanDB only (for local dev) |
+| `make docker-down` | Stop BanyanDB |
+| `make boot` | Build distro + start BanyanDB + boot OAP |
+| `make shutdown` | Stop a running OAP server |
+
+## CI Pipeline
+
+The GitHub Actions CI (`.github/workflows/ci.yml`) automates the full pipeline
on every push to `main`:
+
+1. **License header check** — Apache SkyWalking Eyes
+2. **Build & Test** — compile, javadoc, test, build-distro
+3. **Native image** — builds on both `amd64` and `arm64` runners
+4. **Docker image** — pushes multi-arch manifest to GHCR
+5. **E2E tests** — runs end-to-end tests against the native image
+
+See [Pre-Built Docker Images](docker-image.md) for using the CI-built images
directly.
diff --git a/docs/config-init-immigration.md b/docs/config-init-immigration.md
new file mode 100644
index 0000000..c9f3a25
--- /dev/null
+++ b/docs/config-init-immigration.md
@@ -0,0 +1,253 @@
+# Config Initialization — Eliminate Reflection in Config Loading
+
+## Context
+
+`YamlConfigLoaderUtils.copyProperties()` uses `Field.setAccessible(true)` +
+`field.set()` to populate `ModuleConfig` objects from YAML properties. This
+reflection pattern is problematic for GraalVM native image — every config field
+would require `reflect-config.json` entries, and `setAccessible()` is
restricted.
+
+Since our module/provider set is fixed (37 modules, see distro-policy.md), we
+can generate hardcoded field-setting code at build time that eliminates all
+config-related reflection.
+
+---
+
+## Problem
+
+In `ModuleDefine.prepare()` and in `BanyanDBConfigLoader`,
+`copyProperties()` iterates property names, looks up fields by name via
+reflection, and sets them:
+
+```java
+Field field = getDeclaredField(destClass, propertyName);
+field.setAccessible(true); // restricted in native image
+field.set(dest, value); // needs reflect-config.json
+```
+
+This requires:
+- `getDeclaredField()` with class hierarchy walk
+- `field.setAccessible(true)` to bypass private access
+- `field.set()` for every property
+
+---
+
+## Solution: Same-FQCN Replacement of YamlConfigLoaderUtils
+
+Generate a replacement `YamlConfigLoaderUtils.java` with the same FQCN
+(`org.apache.skywalking.oap.server.library.util.YamlConfigLoaderUtils`) that
+dispatches by config object type and sets fields directly — no
+`Field.setAccessible()`, no `getDeclaredField()` scan.
+
+This is one of 23 same-FQCN replacement classes in the distro (see
distro-policy.md for full list).
+
+### Field Access Strategy (per field)
+
+| Strategy | Condition | Example |
+|---|---|---|
+| **Lombok setter** | All non-final fields (class-level `@Setter` added via
`-for-graalvm` modules) | `cfg.setRole((String) value)` |
+| **Getter + clear + addAll** | Final collection field |
`cfg.getDownsampling().clear(); cfg.getDownsampling().addAll((List) value)` |
+| **Error** | Unknown config type | `throw new
IllegalArgumentException("Unknown config type: ...")` |
+
+All config classes that previously lacked `@Setter` now have it added via
+same-FQCN replacement classes in the `-for-graalvm` modules. No VarHandle, no
+reflection fallback. The generator fails at build time if any non-final field
+lacks a setter.
+
+### Generated Code Structure
+
+```java
+package org.apache.skywalking.oap.server.library.util;
+
+public class YamlConfigLoaderUtils {
+ // No VarHandle, no reflection. Pure setter-based.
+
+ // Type-dispatch: check instanceof, delegate to type-specific method
+ public static void copyProperties(Object dest, Properties src,
+ String moduleName, String providerName)
+ throws IllegalAccessException {
+ if (dest instanceof CoreModuleConfig) {
+ copyToCoreModuleConfig((CoreModuleConfig) dest, src, moduleName,
providerName);
+ } else if (dest instanceof ClusterModuleKubernetesConfig) {
+
copyToClusterModuleKubernetesConfig((ClusterModuleKubernetesConfig) dest, src,
moduleName, providerName);
+ }
+ // ... all config types ...
+ else {
+ throw new IllegalArgumentException("Unknown config type: " +
dest.getClass().getName());
+ }
+ }
+
+ // Per-type: switch on property name, set via Lombok setter
+ private static void copyToCoreModuleConfig(
+ CoreModuleConfig cfg, Properties src,
+ String moduleName, String providerName) {
+ // iterate properties
+ switch (key) {
+ case "role":
+ cfg.setRole((String) value);
+ break;
+ case "persistentPeriod":
+ cfg.setPersistentPeriod((int) value);
+ break;
+ // ... all fields via setters ...
+ default:
+ log.warn("{} setting is not supported in {} provider of {}
module",
+ key, providerName, moduleName);
+ break;
+ }
+ }
+ // ... one method per config type ...
+}
+```
+
+---
+
+## Build Tool
+
+**Module**: `build-tools/config-generator`
+
+**Main class**: `ConfigInitializerGenerator.java`
+
+### Input
+- Classpath with all SkyWalking module JARs (same dependencies as
`oap-graalvm-server`)
+- Provider class list — derived from the fixed module table in
`GraalVMOAPServerStartUp`
+- Extra config class list — BanyanDB nested configs used by
`BanyanDBConfigLoader`
+
+### Process
+1. For each provider, call `newConfigCreator().type()` to discover the config
class
+2. For each config class, use Java reflection to scan all declared fields
+ (walking up to `ModuleConfig` superclass)
+3. For each field, check if a setter method exists (`set` + capitalize(name))
+4. **Fail if any non-final field lacks a setter** (all config classes must
have `@Setter`)
+5. Generate `YamlConfigLoaderUtils.java` with type-dispatch + switch-based
field assignment
+
+The generator depends on `-for-graalvm` modules (not upstream JARs) so it sees
+config classes with `@Setter` added. Original upstream JARs are forced to
+`provided` scope to prevent classpath shadowing.
+
+### Output
+-
`oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/library/util/YamlConfigLoaderUtils.java`
+ (same-FQCN replacement)
+
+### Running the generator
+```bash
+JAVA_HOME=/Users/wusheng/.sdkman/candidates/java/25-graal \
+ mvn -pl build-tools/config-generator exec:java \
+
-Dexec.args="oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/library/util/YamlConfigLoaderUtils.java"
+```
+
+---
+
+## Config Classes Inventory
+
+37 modules registered in `GraalVMOAPServerStartUp`. 1 (`AlarmModule`) has null
+ConfigCreator. The rest need coverage.
+
+### Provider Config Classes
+
+| Provider | Config Class | Fields | Setter Status |
+|---|---|---|---|
+| CoreModuleProvider | `CoreModuleConfig` | ~40 | `@Getter` class-level,
`@Setter` on ~12 fields, rest need VarHandle |
+| BanyanDBStorageProvider | `BanyanDBStorageConfig` | ~5 | `@Getter @Setter` |
+| ClusterModuleStandaloneProvider | (empty config) | 0 | — |
+| ClusterModuleKubernetesProvider | `ClusterModuleKubernetesConfig` | 3 |
`@Setter` |
+| ConfigmapConfigurationProvider | `ConfigmapConfigurationSettings` | 3 |
`@Setter` |
+| PrometheusTelemetryProvider | `PrometheusConfig` | 5 | `@Setter` |
+| AnalyzerModuleProvider | `AnalyzerModuleConfig` | ~10 | `@Getter @Setter` |
+| LogAnalyzerModuleProvider | `LogAnalyzerModuleConfig` | 2 | `@Setter` |
+| SharingServerModuleProvider | `SharingServerConfig` | ~15 | Needs check |
+| EnvoyMetricReceiverProvider | `EnvoyMetricReceiverConfig` | ~15 | Needs
check |
+| KafkaFetcherProvider | `KafkaFetcherConfig` | ~20 | `@Data` (all setters) |
+| ZipkinReceiverProvider | `ZipkinReceiverConfig` | ~20 | Needs check |
+| GraphQLQueryProvider | `GraphQLQueryConfig` | 4 | `@Setter` |
+| HealthCheckerProvider | `HealthCheckerConfig` | 1 | `@Setter` |
+| + others | (simple configs) | 0-5 | Various |
+
+### BanyanDB Config Loading (BanyanDBConfigLoader)
+
+`BanyanDBConfigLoader` (in `storage-banyandb-plugin`) loads config from
`bydb.yml`
+and `bydb-topn.yml` independently of the standard YAML config loading path. It
+calls `copyProperties()` on nested config objects, so the generated
+`YamlConfigLoaderUtils` must handle all BanyanDB inner classes.
+
+**`loadBaseConfig()`** — reads `bydb.yml`, calls `copyProperties()` 11 times:
+
+| Call target | Type | Handler | Fields |
+|---|---|---|---|
+| `config.getGlobal()` | `Global` | `copyToGlobal()` | 18 (targets,
maxBulkSize, flushInterval, ...) |
+| `config.getRecordsNormal()` | `RecordsNormal` | `copyToRecordsNormal()` | 8
(inherited from GroupResource) |
+| `config.getRecordsLog()` | `RecordsLog` | `copyToRecordsLog()` | 8 |
+| `config.getTrace()` | `Trace` | `copyToTrace()` | 8 |
+| `config.getZipkinTrace()` | `ZipkinTrace` | `copyToZipkinTrace()` | 8 |
+| `config.getRecordsBrowserErrorLog()` | `RecordsBrowserErrorLog` |
`copyToRecordsBrowserErrorLog()` | 8 |
+| `config.getMetricsMin()` | `MetricsMin` | `copyToMetricsMin()` | 8 |
+| `config.getMetricsHour()` | `MetricsHour` | `copyToMetricsHour()` | 8 |
+| `config.getMetricsDay()` | `MetricsDay` | `copyToMetricsDay()` | 8 |
+| `config.getMetadata()` | `Metadata` | `copyToMetadata()` | 8 |
+| `config.getProperty()` | `Property` | `copyToProperty()` | 8 |
+
+**`copyStages()`** — creates warm/cold `Stage` objects, calls
`copyProperties()` on each:
+
+| Call target | Type | Handler | Fields |
+|---|---|---|---|
+| warm Stage | `Stage` | `copyToStage()` | 7 (name, nodeSelector, shardNum,
segmentInterval, ttl, replicas, close) |
+| cold Stage | `Stage` | `copyToStage()` | 7 |
+
+**`loadTopNConfig()`** — reads `bydb-topn.yml`, populates `TopN` objects via
direct
+setter calls (`topN.setName()`, `topN.setGroupByTagNames()`, etc.). Does NOT
call
+`copyProperties()` — no handler needed.
+
+**Class hierarchy note**: All group config classes (`RecordsNormal`, `Trace`,
+`MetricsMin`, etc.) extend `GroupResource`. The generator walks the class
hierarchy
+from each subclass up to `GroupResource` and generates setter calls for all 8
+inherited fields (`shardNum`, `segmentInterval`, `ttl`, `replicas`,
+`enableWarmStage`, `enableColdStage`, `defaultQueryStages`,
+`additionalLifecycleStages`). `Trace` doesn't have its own `@Getter @Setter`
but
+inherits all setters from `GroupResource`.
+
+**`instanceof` ordering**: Inner classes (`Global`, `RecordsNormal`, `Trace`,
etc.)
+are static inner classes that do NOT extend `BanyanDBStorageConfig`, so the
+`instanceof BanyanDBStorageConfig` check matches only the top-level config.
Inner
+class checks come after and work independently.
+
+Two extra inner classes (`RecordsTrace`, `RecordsZipkinTrace`) are included in
+EXTRA_CONFIG_CLASSES for completeness, even though `BanyanDBConfigLoader`
doesn't
+currently call `copyProperties()` on them.
+
+---
+
+## Same-FQCN Replacements (Config Initialization)
+
+| Upstream Class | Upstream Location | Replacement Location | What Changed |
+|---|---|---|---|
+| `YamlConfigLoaderUtils` |
`server-library/library-util/.../util/YamlConfigLoaderUtils.java` |
`oap-graalvm-server/` (not in library-util-for-graalvm due to 30+ cross-module
imports) | Complete rewrite. Uses type-dispatch with Lombok `@Setter` methods
instead of `Field.setAccessible()` + `field.set()`. |
+| `CoreModuleConfig` | `server-core/.../core/CoreModuleConfig.java` |
`oap-libs-for-graalvm/server-core-for-graalvm/` | Added `@Setter` at class
level. Upstream only has `@Getter`. |
+| `AnalyzerModuleConfig` |
`analyzer/agent-analyzer/.../provider/AnalyzerModuleConfig.java` |
`oap-libs-for-graalvm/agent-analyzer-for-graalvm/` | Added `@Setter` at class
level. |
+| `LogAnalyzerModuleConfig` |
`analyzer/log-analyzer/.../provider/LogAnalyzerModuleConfig.java` |
`oap-libs-for-graalvm/log-analyzer-for-graalvm/` | Added `@Setter` at class
level. |
+| `EnvoyMetricReceiverConfig` |
`server-receiver-plugin/envoy-metrics-receiver-plugin/.../EnvoyMetricReceiverConfig.java`
| `oap-libs-for-graalvm/envoy-metrics-receiver-for-graalvm/` | Added `@Setter`
at class level. |
+| `OtelMetricReceiverConfig` |
`server-receiver-plugin/otel-receiver-plugin/.../OtelMetricReceiverConfig.java`
| `oap-libs-for-graalvm/otel-receiver-for-graalvm/` | Added `@Setter` at class
level. |
+| `EBPFReceiverModuleConfig` |
`server-receiver-plugin/skywalking-ebpf-receiver-plugin/.../EBPFReceiverModuleConfig.java`
| `oap-libs-for-graalvm/ebpf-receiver-for-graalvm/` | Added `@Setter` at class
level. |
+| `AWSFirehoseReceiverModuleConfig` |
`server-receiver-plugin/aws-firehose-receiver/.../AWSFirehoseReceiverModuleConfig.java`
| `oap-libs-for-graalvm/aws-firehose-receiver-for-graalvm/` | Added `@Setter`
at class level. |
+| `CiliumFetcherConfig` |
`server-fetcher-plugin/cilium-fetcher-plugin/.../CiliumFetcherConfig.java` |
`oap-libs-for-graalvm/cilium-fetcher-for-graalvm/` | Added `@Setter` at class
level. |
+| `StatusQueryConfig` |
`server-query-plugin/status-query-plugin/.../StatusQueryConfig.java` |
`oap-libs-for-graalvm/status-query-for-graalvm/` | Added `@Setter` at class
level. |
+| `HealthCheckerConfig` | `server-health-checker/.../HealthCheckerConfig.java`
| `oap-libs-for-graalvm/health-checker-for-graalvm/` | Added `@Setter` at class
level. |
+
+Config replacements (except `YamlConfigLoaderUtils`) are repackaged into their
respective `-for-graalvm` modules via `maven-shade-plugin`.
`YamlConfigLoaderUtils` lives in `oap-graalvm-server` because it imports types
from 30+ modules; the original `.class` is excluded from
`library-util-for-graalvm` via shade filter.
+
+## Same-FQCN Packaging
+
+The original `YamlConfigLoaderUtils.class` is excluded from the
`library-util-for-graalvm` shaded JAR. The replacement in
`oap-graalvm-server.jar` is the only copy on the classpath.
+
+---
+
+## Verification
+
+```bash
+# Run generator
+JAVA_HOME=/Users/wusheng/.sdkman/candidates/java/25-graal \
+ mvn -pl build-tools/config-generator exec:java \
+
-Dexec.args="oap-graalvm-server/src/main/java/org/apache/skywalking/oap/server/library/util/YamlConfigLoaderUtils.java"
+
+# Full build (compile + test + package)
+JAVA_HOME=/Users/wusheng/.sdkman/candidates/java/25-graal make build-distro
+```
diff --git a/docs/distro-policy.md b/docs/distro-policy.md
new file mode 100644
index 0000000..eefadca
--- /dev/null
+++ b/docs/distro-policy.md
@@ -0,0 +1,322 @@
+# SkyWalking GraalVM Distro - Distribution Policy
+
+## Goal
+Build and package Apache SkyWalking OAP server as a GraalVM native image on
JDK 25.
+
+## Architecture Constraints
+- **Submodule**: `skywalking/` is a git submodule of `apache/skywalking.git`.
All SkyWalking source changes go through upstream PRs. **Minimize upstream
changes.**
+- **This repo**: Maven + Makefile to orchestrate building on top of the
submodule. Pre-compilation, GraalVM config, native-image wiring, and the fixed
module manager live here.
+- **JDK 25**: Already compiles and runs. Not an issue.
+
+## Module Selection (Fixed at Build Time)
+
+| Category | Module | Provider |
+|----------|--------|----------|
+| **Core** | CoreModule | default |
+| **Storage** | StorageModule | BanyanDB |
+| **Cluster** | ClusterModule | Standalone, Kubernetes |
+| **Configuration** | ConfigurationModule | Kubernetes |
+| **Receivers** | SharingServerModule, TraceModule, JVMModule,
MeterReceiverModule, LogModule, RegisterModule, ProfileModule, BrowserModule,
EventModule, OtelMetricReceiverModule, MeshReceiverModule,
EnvoyMetricReceiverModule, ZipkinReceiverModule, ZabbixReceiverModule,
TelegrafReceiverModule, AWSFirehoseReceiverModule, CiliumFetcherModule,
EBPFReceiverModule, AsyncProfilerModule, PprofModule, CLRModule,
ConfigurationDiscoveryModule, KafkaFetcherModule | default providers |
+| **Analyzers** | AnalyzerModule, LogAnalyzerModule, EventAnalyzerModule |
default providers |
+| **Query** | QueryModule (GraphQL), PromQLModule, LogQLModule,
ZipkinQueryModule, StatusQueryModule | default providers |
+| **Alarm** | AlarmModule | default |
+| **Telemetry** | TelemetryModule | Prometheus |
+| **Other** | ExporterModule, HealthCheckerModule, AIPipelineModule | default
providers |
+
+**Full feature set.** Work around issues as they arise.
+
+---
+
+## Core Strategy
+
+1. **Build-Time Class Export**: All runtime code generation (OAL via
Javassist, MAL/LAL via Groovy) runs at build time. Export `.class` files and
package into native-image classpath. Classpath scanning also runs here as a
verification gate.
+
+2. **Fixed Module Wiring**: Module/provider selection is hardcoded in this
distro (no SPI discovery). Simplified config file for selected providers only.
+
+3. **Separation**: SkyWalking upstream changes tracked separately, go through
upstream PRs.
+
+---
+
+## OAL Runtime Class Generation (Javassist)
+
+### What Happens
+OAL V2 generates metrics/builder/dispatcher classes at startup via Javassist
(`ClassPool.makeClass()` → `CtClass.toClass()`). Already has
`writeGeneratedFile()` for debug export.
+
+### Approach (this repo)
+All `.oal` scripts are known. Run OAL engine at build time, export `.class`
files, load them directly at runtime from manifests.
+
+**Details**: [oal-immigration.md](oal-immigration.md)
+
+### What Was Built
+- `OALClassExporter` processes all 9 OAL defines, exports ~620 metrics
classes, ~620 builder classes, ~45 dispatchers
+- 3 manifest files: `oal-metrics-classes.txt`, `oal-dispatcher-classes.txt`,
`oal-disabled-sources.txt`
+- Same-FQCN replacement `OALEngineLoaderService` loads pre-compiled classes
from manifests instead of running Javassist
+
+### Upstream Changes Needed
+- None. Build-time class export works via existing debug API
(`setOpenEngineDebug(true)` + `setGeneratedFilePath()`)
+
+---
+
+## MAL and LAL (Groovy + Javassist)
+
+### What Happens
+- MAL uses `GroovyShell` + `DelegatingScript` for meter rule expressions
(~1250 rules across 71 YAML files). Also, `MeterSystem.create()` uses Javassist
to dynamically generate one meter subclass per metric rule.
+- LAL uses `GroovyShell` + `@CompileStatic` + `LALPrecompiledExtension` for
log analysis scripts (10 rules).
+
+### Approach (this repo)
+Run full MAL/LAL initialization at build time via `build-tools/precompiler`
(unified tool). Export Javassist-generated `.class` files. Transpile all Groovy
expressions to pure Java at build time — zero Groovy at runtime.
+
+**Details**: [mal-immigration.md](mal-immigration.md) |
[lal-immigration.md](lal-immigration.md)
+
+### What Was Built
+- **Unified precompiler** (`build-tools/precompiler`): Replaced separate
`oal-exporter` and `mal-compiler` modules. Compiles all 71 MAL YAML rule files
(meter-analyzer-config, otel-rules, log-mal-rules, envoy-metrics-rules,
telegraf-rules, zabbix-rules) producing 1209 meter classes.
+- **MAL-to-Java transpiler**: 1250+ MAL expressions transpiled from Groovy AST
to pure Java `MalExpression` implementations. 29 filter expressions transpiled
to `MalFilter` implementations. Zero Groovy at runtime.
+- **LAL-to-Java transpiler**: 10 LAL scripts (6 unique) transpiled to pure
Java `LalExpression` implementations. Spec classes enhanced with `Consumer`
overloads for transpiled code.
+- **Groovy stubs module**: Minimal `groovy.lang.*` types (Binding, Closure,
etc.) for class loading. No `org.codehaus.groovy.*` — prevents GraalVM
`GroovyIndyInterfaceFeature` from activating.
+- **Manifests**: `META-INF/mal-expressions.txt` (transpiled Java classes),
`META-INF/mal-groovy-expression-hashes.txt` (SHA-256 for combination pattern
resolution), `META-INF/mal-meter-classes.txt` (Javassist-generated classes),
`META-INF/lal-expressions.txt` (transpiled LAL classes),
`META-INF/annotation-scan/MeterFunction.txt` (16 function classes).
+- **Combination pattern**: Multiple YAML files from different data sources
(otel, telegraf, zabbix) may define metrics with the same name. Deterministic
suffixes (`_1`, `_2`) with expression hash tracking enable unambiguous
resolution.
+- **Same-FQCN replacements**: `DSL.java` (MAL), `DSL.java` (LAL),
`FilterExpression.java`, `MeterSystem.java`, `Expression.java`,
`SampleFamily.java` — all use pure Java, no Groovy.
+- **Comparison test suite**: 73 MAL test classes (1281 assertions) + 5 LAL
test classes (19 assertions) covering all 79 YAML files. Tests require data
flow through full pipeline (no vacuous agreements). Dual-path: fresh Groovy
compilation (Path A) vs transpiled Java (Path B).
+
+### Groovy Elimination
+- MAL: `MalExpression` interface replaces `DelegatingScript`. `SampleFamily`
uses Java functional interfaces (`TagFunction`, `SampleFilter`,
`ForEachFunction`, `DecorateFunction`, `PropertiesExtractor`) instead of
`groovy.lang.Closure`.
+- LAL: `LalExpression` interface replaces `DelegatingScript`. Spec classes
have `Consumer` overloads.
+- No `groovy.lang.Closure` in any production source code. Groovy is test-only
dependency.
+
+---
+
+## Classpath Scanning (Guava ClassPath)
+
+### What Happens
+`ClassPath.from()` used in `SourceReceiverImpl.scan()`, `AnnotationScan`,
`MeterSystem`, `DefaultMetricsFunctionRegistry`, `FilterMatchers`,
`MetricsHolder`.
+
+### What Was Solved
+- `AnnotationScan` and `SourceReceiverImpl` replaced with same-FQCN classes
that read from build-time manifests. 6 annotation/interface manifests under
`META-INF/annotation-scan/`: `ScopeDeclaration`, `Stream`, `Disable`,
`MultipleDisable`, `SourceDispatcher`, `ISourceDecorator`.
+- `DefaultMetricsFunctionRegistry`, `FilterMatchers`, `MetricsHolder` — these
only run inside the OAL engine at build time, not at runtime. Automatically
solved.
+- `MeterSystem` replaced with same-FQCN class that reads from
`META-INF/annotation-scan/MeterFunction.txt` manifest (16 meter function
classes). Solved as part of MAL immigration.
+
+---
+
+## Module System & Configuration
+
+### Current Behavior
+`ModuleManager` uses `ServiceLoader` (SPI). `application.yml` selects
providers. Config loaded via reflection (`Field.setAccessible` + `field.set` in
`YamlConfigLoaderUtils.copyProperties`).
+
+### Approach (this repo)
+1. **New module manager**: Directly constructs chosen
`ModuleDefine`/`ModuleProvider` — no SPI
+2. **Simplified config file**: Only knobs for selected providers
+3. **Config loading**: **No reflection.** Build-time tool scans all
`ModuleConfig` subclass fields → generates same-FQCN replacement of
`YamlConfigLoaderUtils` that uses Lombok setters and VarHandle to set config
fields directly. Eliminates `Field.setAccessible`/`field.set` and the need for
`reflect-config.json` for config classes.
+
+**Details**: [config-init-immigration.md](config-init-immigration.md)
+
+### What Was Built
+- `FixedModuleManager` — direct module/provider construction via
`ModuleDefine.prepare()` overload, no SPI
+- `GraalVMOAPServerStartUp` — entry point with `configuration.has()` guards
for 6 optional modules
+- `application.yml` — simplified config for selected providers
+- `ConfigInitializerGenerator` — build-time tool that scans config classes and
generates `YamlConfigLoaderUtils` replacement
+- `YamlConfigLoaderUtils` — same-FQCN replacement using type-dispatch +
setter/VarHandle instead of reflection
+- `ModuleDefine` — same-FQCN replacement (`library-module-for-graalvm`) adding
`prepare(ModuleManager, ModuleProvider, ...)` overload for direct provider
wiring without ServiceLoader
+
+---
+
+## Same-FQCN Packaging (Repackaged Modules)
+
+### Problem
+
+Same-FQCN replacement classes need to shadow upstream originals. Classpath
ordering tricks confuse developers and AI tools.
+
+### Solution: Per-JAR Repackaged Modules (`oap-libs-for-graalvm`)
+
+Each upstream JAR that has replacement classes gets a corresponding
`*-for-graalvm` module under `oap-libs-for-graalvm/`. The module uses
`maven-shade-plugin` to:
+1. Include only the upstream JAR in the shade
+2. Exclude the specific `.class` files being replaced
+3. Produce a JAR containing: all upstream classes MINUS replaced ones PLUS our
replacements
+
+`oap-graalvm-server` depends on `*-for-graalvm` JARs instead of originals.
Original upstream JARs are forced to `provided` scope via
`<dependencyManagement>` to prevent transitive leakage.
+
+### 23 Same-FQCN Replacement Classes Across 13 Modules
+
+**Non-trivial replacements (load pre-compiled assets from manifests):**
+
+| Module | Replacement Classes | Purpose |
+|---|---|---|
+| `library-module-for-graalvm` | `ModuleDefine` | Add `prepare()` overload for
direct provider wiring (bypasses ServiceLoader) |
+| `server-core-for-graalvm` | `OALEngineLoaderService`, `AnnotationScan`,
`SourceReceiverImpl`, `MeterSystem`, `CoreModuleConfig`,
`HierarchyDefinitionService` | Load from manifests instead of
Javassist/ClassPath; config with @Setter; Java-backed closures instead of
GroovyShell |
+| `library-util-for-graalvm` | `YamlConfigLoaderUtils` | Set config fields via
setter instead of reflection |
+| `meter-analyzer-for-graalvm` | `DSL`, `FilterExpression`, `Rules` | Load
pre-compiled MAL Groovy scripts from manifest; load rule data from JSON
config-data manifests |
+| `log-analyzer-for-graalvm` | `DSL`, `LogAnalyzerModuleConfig`, `LALConfigs`
| Load pre-compiled LAL scripts; config with @Setter; load LAL config data from
JSON config-data manifests |
+| `agent-analyzer-for-graalvm` | `AnalyzerModuleConfig`, `MeterConfigs` |
Config with @Setter; load meter config data from JSON config-data manifests |
+
+**Config-only replacements (add `@Setter` for reflection-free config):**
+
+| Module | Replacement Class |
+|---|---|
+| `envoy-metrics-receiver-for-graalvm` | `EnvoyMetricReceiverConfig` |
+| `otel-receiver-for-graalvm` | `OtelMetricReceiverConfig` |
+| `ebpf-receiver-for-graalvm` | `EBPFReceiverModuleConfig` |
+| `aws-firehose-receiver-for-graalvm` | `AWSFirehoseReceiverModuleConfig` |
+| `cilium-fetcher-for-graalvm` | `CiliumFetcherConfig` |
+| `status-query-for-graalvm` | `StatusQueryConfig` |
+| `health-checker-for-graalvm` | `HealthCheckerConfig` |
+
+### No Classpath Ordering Required
+
+No duplicate FQCNs on the classpath. The startup script (`oapService.sh`) uses
a simple flat classpath. The `oap-graalvm-native` uber JAR also has no FQCN
conflicts.
+
+### Adding New Replacements
+
+To add a new same-FQCN replacement:
+1. Create a new `*-for-graalvm` module under `oap-libs-for-graalvm/` (or add
to existing one)
+2. Add the replacement `.java` file with the same FQCN
+3. Configure shade plugin to exclude the original `.class` from the upstream
JAR
+4. Add the `-for-graalvm` artifact to root `pom.xml` `<dependencyManagement>`
+5. In `oap-graalvm-server/pom.xml`: add the original JAR to
`<dependencyManagement>` as `provided`, add `-for-graalvm` to `<dependencies>`
+6. Add the original JAR to `distribution.xml` `<excludes>`
+
+---
+
+## Additional GraalVM Risks
+
+| Risk | Status | Mitigation |
+|------|--------|------------|
+| **Reflection** (annotations, OAL enricher, HTTP handlers, GraphQL types) |
SOLVED | Auto-generated by precompiler from manifests;
`log4j2-reflect-config.json` for Log4j2 plugins |
+| **gRPC / Netty / Armeria** | SOLVED | GraalVM reachability metadata repo
handles these automatically |
+| **Resource loading** (`ResourceUtils`, config files) | SOLVED |
`resource-config.json` via tracing agent |
+| **Log4j2** | SOLVED | Console-only `log4j2.xml` avoids RollingFile
reflection chain; Log4j2 plugin classes in `log4j2-reflect-config.json` |
+| **Kafka client** (for Kafka fetcher) | Untested | Known GraalVM support, may
need config |
+| **Kubernetes client 6.7.1** (for cluster + config) | Untested | Has GraalVM
support, may need config at runtime |
+
+---
+
+## Distro Resource Files
+
+Upstream `server-starter/src/main/resources/` contains 236 files. They fall
into
+two categories: files included directly in the distro `config/` directory
(loaded
+at runtime via file I/O), and files consumed by the precompiler at build time
+(not needed at runtime — their logic is baked into pre-compiled `.class`
files).
+
+### Directly Included in Distro (`config/`)
+
+These files are loaded at runtime via `ResourceUtils.read()`, `Files.walk()`,
or
+YAML parsing. No reflection involved — safe for GraalVM native image as-is.
+
+| File / Directory | Count | Loaded By | Purpose |
+|---|---|---|---|
+| `application.yml` | 1 | Custom (distro's own, not upstream) |
Module/provider config |
+| `bydb.yml` | 1 | `BanyanDBConfigLoader` | BanyanDB storage base config |
+| `bydb-topn.yml` | 1 | `BanyanDBConfigLoader` | BanyanDB TopN aggregation
config |
+| `log4j2.xml` | 1 | Log4j2 framework | Logging configuration |
+| `alarm-settings.yml` | 1 | `AlarmModuleProvider` via `ResourceUtils.read()`
| Alarm rules |
+| `component-libraries.yml` | 1 | `ComponentLibraryCatalogService` via
`ResourceUtils.read()` | Component ID mapping |
+| `endpoint-name-grouping.yml` | 1 | `EndpointNameGroupingRuleWatcher` via
`ResourceUtils.read()` | Endpoint grouping rules |
+| `gateways.yml` | 1 | `UninstrumentedGatewaysConfig` via
`ResourceUtils.read()` | Gateway definitions |
+| `hierarchy-definition.yml` | 1 | `HierarchyDefinitionService` via
`ResourceUtils.read()` | Layer hierarchy |
+| `metadata-service-mapping.yaml` | 1 | `ResourceUtils.read()` | Metadata
service mapping |
+| `service-apdex-threshold.yml` | 1 | `ApdexThresholdConfig` via
`ResourceUtils.read()` | APDEX thresholds |
+| `trace-sampling-policy-settings.yml` | 1 | `TraceSamplingPolicyWatcher` via
`ResourceUtils.read()` | Trace sampling |
+| `ui-initialized-templates/**` | 131 | `UITemplateInitializer` via
`Files.walk()` | UI dashboard JSON templates |
+| `cilium-rules/**` | 2 | `CiliumFetcherProvider` via
`ResourceUtils.getPathFiles()` | Cilium flow rules |
+| `openapi-definitions/**` | 1 | `EndpointNameGrouping` via
`ResourceUtils.getPathFiles()` | OpenAPI grouping definitions |
+
+**Total: 146 files** included in the distro `config/` directory.
+
+### Pre-compiled at Build Time (NOT in distro)
+
+These files are consumed by `build-tools/precompiler` during the build. Their
+expressions, scripts, and metric definitions are compiled into `.class` files
+packaged in JARs. The YAML source files are not needed at runtime.
+
+| Category | Count | Pre-compiled Into | Tool |
+|---|---|---|---|
+| `oal/*.oal` | 9 | ~620 metrics classes + ~620 builders + ~45 dispatchers
(Javassist) | `OALClassExporter` |
+| `meter-analyzer-config/*.yaml` | 11 | 147 Groovy scripts + Javassist meter
classes | `MALPrecompiler` |
+| `otel-rules/**/*.yaml` | 55 | 1044 Groovy scripts + Javassist meter classes
| `MALPrecompiler` |
+| `log-mal-rules/*.yaml` | 2 | 2 Groovy scripts | `MALPrecompiler` |
+| `envoy-metrics-rules/*.yaml` | 2 | 26 Groovy scripts + Javassist meter
classes | `MALPrecompiler` |
+| `telegraf-rules/*.yaml` | 1 | 20 Groovy scripts + Javassist meter classes |
`MALPrecompiler` |
+| `zabbix-rules/*.yaml` | 1 | 15 Groovy scripts + Javassist meter classes |
`MALPrecompiler` |
+| `lal/*.yaml` | 8 | 6 unique `@CompileStatic` Groovy classes |
`LALPrecompiler` |
+
+**Total: 89 files** consumed at build time, producing ~1285 pre-compiled
classes
+and ~1254 Groovy scripts stored in JARs.
+
+Additionally, the precompiler serializes parsed config POJOs as JSON manifests
in
+`META-INF/config-data/` (7 JSON files for meter-analyzer-config, otel-rules,
+envoy-metrics-rules, log-mal-rules, telegraf-rules, zabbix-rules, and lal).
These
+provide the runtime "wiring" data (metric prefixes, rule names, expression
lookup
+keys) that replacement loader classes use instead of filesystem YAML access.
+
+### Not Included (upstream-only)
+
+| File | Reason |
+|---|---|
+| `application.yml` (upstream) | Replaced by distro's own simplified
`application.yml` |
+
+---
+
+## Build Workflow
+
+### Build System
+- Maven + Makefile orchestrates building on top of the skywalking submodule
+- GraalVM JDK 25 in CI (`.github/workflows/ci.yml`)
+- JVM-mode starter with fixed module wiring (`FixedModuleManager` +
`GraalVMOAPServerStartUp`)
+- Simplified config file for selected modules (`application.yml`)
+
+### Build-Time Pre-Compilation
+
+**OAL**: OAL engine exports `.class` files (9 defines, ~620 metrics, ~620
builders, ~45 dispatchers). 7 annotation/interface manifests. 3 same-FQCN
replacement classes (`OALEngineLoaderService`, `AnnotationScan`,
`SourceReceiverImpl`).
+
+**MAL**: Unified precompiler (`build-tools/precompiler`) processes 71 YAML
files → 1250 expressions transpiled to pure Java `MalExpression` + 1209
Javassist meter classes. Combination pattern with deterministic suffixes +
expression hash tracking. Same-FQCN replacements: `DSL.java`,
`FilterExpression.java`, `MeterSystem.java`, `Expression.java`,
`SampleFamily.java`. 73 comparison test classes, 1281 assertions (100% YAML
coverage).
+
+**LAL**: 8 YAML files → 10 rules → 6 unique transpiled Java `LalExpression`
classes. Same-FQCN `DSL.java` loads via SHA-256 hash lookup. 5 comparison test
classes, 19 assertions (100% branch coverage).
+
+**Config initialization**: `ConfigInitializerGenerator` generates same-FQCN
`YamlConfigLoaderUtils` using Lombok setters — zero `Field.setAccessible` at
runtime.
+
+**Config data serialization**: Precompiler serializes parsed config POJOs to
`META-INF/config-data/*.json` (7 JSON files). 3 same-FQCN replacement loaders
(`MeterConfigs`, `Rules`, `LALConfigs`) deserialize from JSON instead of
filesystem YAML.
+
+**Module system**: `ModuleDefine` replacement with direct `prepare()` overload
(bypasses ServiceLoader). `GraalVMOAPServerStartUp` with `configuration.has()`
guards for 6 optional modules.
+
+**Distro resource packaging**: 146 runtime files → distro `config/`, 89
pre-compiled files → JARs. Assembly descriptor (`distribution.xml`) packages
runtime config files from upstream.
+
+### Groovy Elimination
+
+- MAL-to-Java transpiler: 1250+ expressions → pure Java `MalExpression` (no
Groovy MOP/ExpandoMetaClass)
+- LAL-to-Java transpiler: 10 scripts → pure Java `LalExpression` (no
DelegatingScript)
+- `SampleFamily` Closure parameters → Java functional interfaces (zero
`groovy.lang.Closure` in production)
+- Groovy stubs module for class loading (no `org.codehaus.groovy.*`)
+- HierarchyDefinitionService: same-FQCN replacement with Java-backed closures
+- Real Groovy (`groovy-5.0.3.jar`) is test-only;
`groovy-stubs-1.0.0-SNAPSHOT.jar` on runtime classpath
+- 1303 tests require actual data flow (no vacuous empty-result agreements)
+
+### Native Image Build
+
+- `native-maven-plugin` (GraalVM buildtools 0.10.4) in `oap-graalvm-native`
with `-Pnative` profile
+- `reflect-config.json` auto-generated by precompiler from manifests (OAL,
MAL, LAL, meter, HTTP handlers, GraphQL types)
+- `log4j2-reflect-config.json` for Log4j2 plugin classes; console-only
`log4j2.xml` with `SW_LOG_LEVEL` env var
+- gRPC/Netty/Protobuf/Armeria via GraalVM reachability metadata repository
+- Auto-scanned reflection metadata: Armeria HTTP handlers (~19), GraphQL
resolvers (~32), GraphQL types (~182), config POJOs (8)
+- Native binary: ~203MB, boots to full module init with all HTTP endpoints
functional
+
+### Native Distro Packaging
+
+- Assembly descriptor (`native-distribution.xml`) packages native binary +
config files
+- `Dockerfile.native` packages native distro into `debian:bookworm-slim`
+- `docker-compose.yml` with BanyanDB + OAP native services
+- CI pipeline: multi-arch native build (amd64 + arm64) with Docker manifest
push to GHCR
+
+### Remaining Verification
+- Verify all receiver plugins work (gRPC + HTTP endpoints)
+- Verify all query APIs work (GraphQL, PromQL, LogQL, Zipkin)
+- Verify cluster mode (K8s)
+- Verify alarm module
+- Performance benchmarking vs JVM
+
+---
+
+## Upstream Changes Tracker
+
+No upstream changes needed. All GraalVM incompatibilities are resolved in this
distro via same-FQCN replacement and build-time pre-compilation:
+- OAL: build-time class export works via existing debug API
+- MAL: transpiled to pure Java, bypasses Groovy entirely
+- LAL: transpiled to pure Java, bypasses Groovy entirely
+- Dynamic Groovy MOP: transpiled to pure Java, no ExpandoMetaClass/MOP at
runtime
diff --git a/docs/docker-image.md b/docs/docker-image.md
new file mode 100644
index 0000000..ecb75b4
--- /dev/null
+++ b/docs/docker-image.md
@@ -0,0 +1,108 @@
+# Pre-Built Docker Images
+
+Pre-built multi-architecture Docker images are published to **GitHub Container
Registry (GHCR)**
+on every push to the `main` branch via CI.
+
+## Image Repository
+
+```
+ghcr.io/apache/skywalking-graalvm-distro
+```
+
+GitHub Packages page:
[https://github.com/apache/skywalking-graalvm-distro/pkgs/container/skywalking-graalvm-distro](https://github.com/apache/skywalking-graalvm-distro/pkgs/container/skywalking-graalvm-distro)
+
+## Tags
+
+| Tag | Description |
+|-----|-------------|
+| `latest` | Most recent build from the `main` branch |
+| `<short-sha>` | Pinned to a specific commit (first 7 characters of the
commit SHA) |
+
+## Supported Architectures
+
+Each tag is a multi-architecture manifest supporting:
+
+- `linux/amd64`
+- `linux/arm64`
+
+Docker automatically selects the correct architecture for your platform.
+
+## Pull the Image
+
+```bash
+docker pull ghcr.io/apache/skywalking-graalvm-distro:latest
+```
+
+Or pin to a specific commit:
+
+```bash
+docker pull ghcr.io/apache/skywalking-graalvm-distro:<short-sha>
+```
+
+## Run with Docker
+
+### Standalone with BanyanDB
+
+The simplest way is to use the included `docker-compose.yml`, replacing the
locally built image
+with the pre-built one:
+
+```bash
+# Clone the repo (for docker-compose.yml and config files only)
+git clone https://github.com/apache/skywalking-graalvm-distro.git
+cd skywalking-graalvm-distro
+
+# Run with pre-built image
+docker compose -f docker/docker-compose.yml up
+```
+
+Edit `docker/docker-compose.yml` to use the GHCR image instead of the local
build:
+
+```yaml
+services:
+ oap:
+ image: ghcr.io/apache/skywalking-graalvm-distro:latest
+ # ... rest of config unchanged
+```
+
+### Manual Docker Run
+
+```bash
+docker run -d \
+ -p 12800:12800 \
+ -p 11800:11800 \
+ -e SW_STORAGE_BANYANDB_TARGETS=<banyandb-host>:17912 \
+ -e SW_CLUSTER=standalone \
+ -e SW_CONFIGURATION=none \
+ ghcr.io/apache/skywalking-graalvm-distro:latest
+```
+
+## Environment Variables
+
+All settings from [Configuration](configuration.md) are configurable via
environment variables.
+Common ones for Docker deployment:
+
+| Variable | Description | Default |
+|----------|-------------|---------|
+| `SW_STORAGE_BANYANDB_TARGETS` | BanyanDB server address | `127.0.0.1:17912` |
+| `SW_CLUSTER` | Cluster mode (`standalone` or `kubernetes`) | `standalone` |
+| `SW_CONFIGURATION` | Dynamic config provider (`none` or `k8s-configmap`) |
`k8s-configmap` |
+| `SW_CORE_REST_PORT` | REST API port | `12800` |
+| `SW_CORE_GRPC_PORT` | gRPC port | `11800` |
+| `SW_TELEMETRY_PROMETHEUS_PORT` | Prometheus metrics port | `1234` |
+| `SW_LOG_LEVEL` | Log4j2 log level | `INFO` |
+| `JAVA_OPTS` | Additional flags passed to the native binary (e.g.,
`-Dmode=init`) | *(empty)* |
+
+## Exposed Ports
+
+| Port | Protocol | Service |
+|------|----------|---------|
+| 12800 | HTTP | REST API — GraphQL query, health check, HTTP data receivers |
+| 11800 | gRPC | Agent data collection — traces, metrics, logs, profiling |
+| 1234 | HTTP | Prometheus self-telemetry metrics |
+
+## Image Details
+
+- **Base image**: `debian:bookworm-slim`
+- **Contents**: Native binary (`oap-server`, ~203MB) + `config/` directory
+- **Entrypoint**: `./oap-server` with `JAVA_OPTS` forwarded as command-line
arguments
+- **No JVM**: The image does not contain a JVM — the binary is a
self-contained GraalVM native image
diff --git a/docs/lal-immigration.md b/docs/lal-immigration.md
new file mode 100644
index 0000000..d22704f
--- /dev/null
+++ b/docs/lal-immigration.md
@@ -0,0 +1,352 @@
+# LAL Immigration: Build-Time Pre-Compilation + Groovy Elimination
+
+## Context
+
+LAL (Log Analysis Language) uses Groovy with `@CompileStatic` +
`LALPrecompiledExtension` for log analysis scripts. At startup,
`GroovyShell.parse()` compiles each LAL DSL script into a
`LALDelegatingScript`. GraalVM native image cannot compile Groovy at runtime.
+
+LAL also enforces security constraints via `SecureASTCustomizer` — `while`,
`do-while`, and `for` loops are disallowed. Branch coverage focuses on
`if`/`else if`/`else` chains.
+
+**Solution**: Compile all LAL scripts at build time via the unified
precompiler. Export `.class` files + manifests. At runtime, load pre-compiled
classes via SHA-256 hash lookup — no Groovy compilation.
+
+---
+
+## Rule File Inventory
+
+**8 LAL YAML files, 10 rules, 6 unique pre-compiled classes:**
+
+| File | Rule Name | DSL Features |
+|---|---|---|
+| `default.yaml` | default | Empty sink (trivial passthrough) |
+| `nginx.yaml` | nginx-access-log | `tag()` guard, `text { regexp }`,
conditional tag extraction |
+| `nginx.yaml` | nginx-error-log | `tag()` guard, `text { regexp }`, timestamp
with format, `metrics {}` |
+| `mysql-slowsql.yaml` | mysql-slowsql | `json {}`, conditional `slowSql {}` |
+| `pgsql-slowsql.yaml` | pgsql-slowsql | Identical DSL to mysql-slowsql |
+| `redis-slowsql.yaml` | redis-slowsql | Identical DSL to mysql-slowsql |
+| `envoy-als.yaml` | envoy-als | `parsed?.` navigation, conditional `abort
{}`, tag extraction, `rateLimit` sampler |
+| `envoy-als.yaml` | network-profiling-slow-trace | `json {}`, `tag()` guard,
`sampledTrace {}` with 3-way if/else chains |
+| `mesh-dp.yaml` | network-profiling-slow-trace | Identical DSL to envoy-als's
2nd rule |
+| `k8s-service.yaml` | network-profiling-slow-trace | Identical DSL to
envoy-als's 2nd rule |
+
+**SHA-256 deduplication**: mysql/pgsql/redis share identical DSL (1 class).
The 3 network-profiling rules share identical DSL (1 class). Total unique
pre-compiled classes: **6**.
+
+---
+
+## Build-Time Compilation
+
+The unified precompiler (`build-tools/precompiler`) handles LAL alongside OAL
and MAL:
+
+1. Loads all 8 LAL YAML files via `LALConfigs.load()`
+2. For each rule's DSL string, compiles with the same `CompilerConfiguration`
as upstream:
+ - `@CompileStatic` with `LALPrecompiledExtension` for type checking
+ - `SecureASTCustomizer` disallowing loops
+ - `ImportCustomizer` for `ProcessRegistry`
+ - Script base class: `LALDelegatingScript`
+3. Exports compiled `.class` files to the output directory
+4. Writes three manifest files:
+
+**`META-INF/lal-expressions.txt`** — SHA-256 hash → transpiled Java class
(used at runtime):
+```
+a1b2c3d4...=org.apache.skywalking.oap.server.core.source.oal.rt.lal.LalExpr_0
+e5f6a7b8...=org.apache.skywalking.oap.server.core.source.oal.rt.lal.LalExpr_1
+...
+```
+
+**`META-INF/lal-scripts-by-hash.txt`** — SHA-256 hash → Groovy class
(build-time artifact):
+```
+a1b2c3d4...=network_profiling_slow_trace
+...
+```
+
+**`META-INF/lal-scripts.txt`** — rule name → Groovy class (build-time
artifact):
+```
+default=default
+nginx-access-log=nginx_access_log
+...
+```
+
+The `lal-expressions.txt` manifest is the runtime manifest for the transpiled
Java expressions.
+The other two manifests are build-time artifacts for verification and
debugging.
+
+---
+
+## Runtime Replacement
+
+**Same-FQCN class**: `oap-graalvm-server/.../log/analyzer/dsl/DSL.java`
+
+Same FQCN as upstream `org.apache.skywalking.oap.log.analyzer.dsl.DSL`. The
`of()` method:
+
+1. Computes SHA-256 of the DSL string
+2. Loads `META-INF/lal-scripts-by-hash.txt` manifest (lazy, thread-safe,
cached)
+3. Looks up the pre-compiled class name by hash
+4. `Class.forName(className)` → `newInstance()` → cast to `DelegatingScript`
+5. Creates `FilterSpec`, sets delegate, returns `DSL` instance
+
+No `GroovyShell`, no compilation. The pre-compiled class already contains the
statically-compiled bytecode with all type checking baked in.
+
+---
+
+## Key Difference from MAL
+
+| Aspect | MAL | LAL |
+|---|---|---|
+| Groovy mode | Dynamic (MOP, `propertyMissing`, `ExpandoMetaClass`) |
`@CompileStatic` with extension |
+| Loop support | No restriction | Loops disallowed (`SecureASTCustomizer`) |
+| Script base class | `DelegatingScript` | `LALDelegatingScript` |
+| Manifest lookup | By metric name | By SHA-256 hash of DSL content |
+| GraalVM risk | High (dynamic Groovy MOP) | Low (statically compiled) |
+
+LAL's `@CompileStatic` compilation means the pre-compiled classes are fully
statically typed — no runtime metaclass manipulation needed. This makes LAL
significantly more native-image-friendly than MAL.
+
+---
+
+## Comparison Test Suite
+
+**5 test classes, 19 assertions** covering all 8 YAML files and 10 rules.
+
+Each test runs both paths and asserts identical `Binding` state:
+
+```
+ LAL YAML file
+ |
+ Load rules (name, dsl)
+ |
+ For each rule's DSL:
+ / \
+ Path A (Fresh Groovy) Path B (Pre-compiled)
+ GroovyShell.parse() SHA-256 → manifest lookup
+ same CompilerConfig Class.forName() → newInstance()
+ \ /
+ Create FilterSpec (mocked ModuleManager)
+ script.setDelegate(filterSpec)
+ filterSpec.bind(binding)
+ script.run()
+ \ /
+ Assert identical Binding state
+```
+
+**What is compared after evaluation:**
+- `binding.shouldAbort()` — did `abort {}` fire?
+- `binding.shouldSave()` — log persistence flag
+- `binding.log()` — LogData.Builder state (service, serviceInstance, endpoint,
layer, timestamp, tags)
+- `binding.metricsContainer()` — SampleFamily objects (for nginx-error-log
`metrics {}`)
+- `binding.databaseSlowStatement()` — builder state (for slowSql rules)
+- `binding.sampledTraceBuilder()` — builder state (for network-profiling
sampledTrace)
+
+### Test Classes
+
+| Test Class | YAML File(s) | Tests | Coverage |
+|---|---|---|---|
+| `LALDefaultTest` | default.yaml | 2 | Trivial passthrough + manifest
verification |
+| `LALNginxTest` | nginx.yaml | 5 | Access log: matching/non-matching tag +
non-matching regex. Error log: matching/non-matching tag |
+| `LALSlowSqlTest` | mysql/pgsql/redis-slowsql.yaml | 3 | SLOW_SQL tag guard
(match/skip) + 3-file manifest verification |
+| `LALEnvoyAlsTest` | envoy-als.yaml (1st rule) | 3 | Abort path (low code, no
flags), non-abort with flags, non-abort with high code |
+| `LALNetworkProfilingTest` | envoy-als/mesh-dp/k8s-service.yaml | 6 | 4
componentId branches (http/tcp x ssl/no-ssl), LOG_KIND guard, 3-file manifest
verification |
+
+### Branch Coverage
+
+- **`tag()` guard**: All rules with `if (tag("LOG_KIND") == ...)` tested with
matching and non-matching values
+- **`abort {}`**: envoy-als tested with conditions that trigger and skip abort
+- **`slowSql {}`**: Tested with SLOW_SQL tag match (block executed) and
non-match (block skipped)
+- **`sampledTrace {}`**: componentId 4-way if/else chain fully covered
(HTTP=49, HTTPS=129, TLS=130, TCP=110)
+- **`text { regexp }`**: nginx access log tested with matching and
non-matching text patterns
+- **`json {}`**: Tested via slowSql and network-profiling rules
+- **`rateLimit` sampler**: envoy-als tested with responseFlags present/absent
(if/else branch)
+- **`parsed?.` navigation**: envoy-als tested with nested Map traversal
+
+---
+
+## Config Data Serialization
+
+At build time, the precompiler serializes parsed LAL config POJOs to a JSON
manifest at
+`META-INF/config-data/lal.json`. This provides the runtime config data (rule
names, DSL
+strings, layers) for `LogFilterListener` to create `DSL` instances — without
requiring
+filesystem access to the original YAML files.
+
+| JSON Manifest | Source Directory | Serialized Type |
+|---|---|---|
+| `lal.json` | `lal/` | `Map<String, LALConfigs>` (filename → configs) |
+
+At runtime, the replacement `LALConfigs.load()` deserializes from this JSON
file instead
+of reading YAML from the filesystem.
+
+---
+
+## Same-FQCN Replacements (LAL)
+
+| Upstream Class | Upstream Location | Replacement Location | What Changed |
+|---|---|---|---|
+| `DSL` (LAL) | `analyzer/log-analyzer/.../dsl/DSL.java` |
`oap-libs-for-graalvm/log-analyzer-for-graalvm/` | Complete rewrite. Loads
transpiled `LalExpression` from `META-INF/lal-expressions.txt` manifest (keyed
by SHA-256 hash). No Groovy runtime. |
+| `LogAnalyzerModuleConfig` |
`analyzer/log-analyzer/.../provider/LogAnalyzerModuleConfig.java` |
`oap-libs-for-graalvm/log-analyzer-for-graalvm/` | Added `@Setter` at class
level. Enables reflection-free config loading via Lombok setters. |
+| `LALConfigs` | `analyzer/log-analyzer/.../provider/LALConfigs.java` |
`oap-libs-for-graalvm/log-analyzer-for-graalvm/` | Complete rewrite of static
`load()` method. Loads pre-compiled LAL config data from
`META-INF/config-data/{path}.json` instead of filesystem YAML files via
`ResourceUtils.getPathFiles()`. |
+| `AbstractSpec` | `analyzer/log-analyzer/.../dsl/spec/AbstractSpec.java` |
`oap-libs-for-graalvm/log-analyzer-for-graalvm/` | Added `abort()` no-arg
overload for transpiled code. |
+| `FilterSpec` | `analyzer/log-analyzer/.../dsl/spec/filter/FilterSpec.java` |
`oap-libs-for-graalvm/log-analyzer-for-graalvm/` | Added `Consumer` overloads:
`json()`, `text(Consumer)`, `extractor(Consumer)`, `sink(Consumer)`,
`filter(Runnable)`. |
+| `ExtractorSpec` |
`analyzer/log-analyzer/.../dsl/spec/extractor/ExtractorSpec.java` |
`oap-libs-for-graalvm/log-analyzer-for-graalvm/` | Added `Consumer` overloads:
`metrics(Consumer)`, `slowSql(Consumer)`, `sampledTrace(Consumer)`. |
+| `SinkSpec` | `analyzer/log-analyzer/.../dsl/spec/sink/SinkSpec.java` |
`oap-libs-for-graalvm/log-analyzer-for-graalvm/` | Added `Consumer` overloads:
`sampler(Consumer)`, `enforcer()`, `dropper()`. |
+| `SamplerSpec` | `analyzer/log-analyzer/.../dsl/spec/sink/SamplerSpec.java` |
`oap-libs-for-graalvm/log-analyzer-for-graalvm/` | Added `rateLimit(String,
Consumer)`, `possibility(int, Consumer)` for String-keyed samplers. |
+
+All replacements are repackaged into `log-analyzer-for-graalvm` via
`maven-shade-plugin` — the original `.class` files are excluded from the shaded
JAR.
+
+---
+
+## Files Created
+
+1.
**`oap-libs-for-graalvm/log-analyzer-for-graalvm/src/main/java/.../log/analyzer/dsl/DSL.java`**
+ Same-FQCN replacement: loads pre-compiled LAL scripts from manifest via
SHA-256 hash
+
+2.
**`oap-graalvm-server/src/test/java/.../graalvm/lal/LALScriptComparisonBase.java`**
+ Abstract base class: ModuleManager mock setup, dual-path compilation,
Binding state comparison
+
+3. **`oap-graalvm-server/src/test/java/.../graalvm/lal/LALDefaultTest.java`**
+ Tests for default.yaml (2 tests)
+
+4. **`oap-graalvm-server/src/test/java/.../graalvm/lal/LALNginxTest.java`**
+ Tests for nginx.yaml access-log and error-log rules (5 tests)
+
+5. **`oap-graalvm-server/src/test/java/.../graalvm/lal/LALSlowSqlTest.java`**
+ Tests for mysql/pgsql/redis-slowsql.yaml (3 tests)
+
+6. **`oap-graalvm-server/src/test/java/.../graalvm/lal/LALEnvoyAlsTest.java`**
+ Tests for envoy-als.yaml envoy-als rule (3 tests)
+
+7.
**`oap-graalvm-server/src/test/java/.../graalvm/lal/LALNetworkProfilingTest.java`**
+ Tests for network-profiling-slow-trace rule across 3 files (6 tests)
+
+---
+
+## Verification
+
+```bash
+# Build precompiler first (generates LAL classes + manifests)
+JAVA_HOME=/Users/wusheng/.sdkman/candidates/java/25-graal \
+ mvn -pl build-tools/precompiler install -DskipTests
+
+# Run LAL tests only
+JAVA_HOME=/Users/wusheng/.sdkman/candidates/java/25-graal \
+ mvn -pl oap-graalvm-server test \
+ -Dtest="org.apache.skywalking.oap.server.graalvm.lal.*"
+
+# Full build (all tests)
+JAVA_HOME=/Users/wusheng/.sdkman/candidates/java/25-graal make build-distro
+```
+
+Expected: 19 comparison tests across 5 test classes, all passing.
+
+---
+
+## Pure Java LAL Transpiler
+
+LAL transpilation is complete. All 10 LAL scripts (6 unique after SHA-256
dedup)
+are transpiled from Groovy AST to pure Java source at build time, compiled to
+`.class` files, and loaded at runtime via `LalExpression` interface — no Groovy
+runtime needed.
+
+**Approach**: Both options from the plan were combined:
+- **Option A (Transpiler)**: `LalToJavaTranspiler` converts Groovy AST to Java
source
+- **Option B (Groovy Stubs)**: `groovy-stubs` module provides minimal
`groovy.lang.*`
+ types for class loading (no `org.codehaus.groovy.*`)
+
+---
+
+## What Was Built
+
+### 1. LalExpression Interface
+`oap-libs-for-graalvm/log-analyzer-for-graalvm/.../dsl/LalExpression.java`
+
+```java
+@FunctionalInterface
+public interface LalExpression {
+ void execute(FilterSpec filterSpec, Binding binding);
+}
+```
+
+### 2. Groovy Stubs Module
+`oap-libs-for-graalvm/groovy-stubs/` — Minimal stub classes:
+- `groovy.lang.Binding`, `Closure`, `GString`, `GroovyObject`,
`GroovyObjectSupport`
+- `groovy.lang.Script`, `groovy.util.DelegatingScript`
+- `groovy.lang.DelegatesTo`, `MetaClass`, `MissingPropertyException`,
`GroovyRuntimeException`
+
+Key: **No `org.codehaus.groovy.*` packages** — prevents GraalVM
`GroovyIndyInterfaceFeature` from activating.
+
+### 3. Spec Class Consumer Overloads
+Same-FQCN replacements in `oap-libs-for-graalvm/log-analyzer-for-graalvm/`:
+- `AbstractSpec` — `abort()` no-arg
+- `FilterSpec` — `json()` no-arg, `text(Consumer)`, `extractor(Consumer)`,
`sink(Consumer)`, `filter(Runnable)`
+- `ExtractorSpec` — `metrics(Consumer)`, `slowSql(Consumer)`,
`sampledTrace(Consumer)`
+- `SinkSpec` — `sampler(Consumer)`, `enforcer()`, `dropper()`
+- `SamplerSpec` — `rateLimit(String, Consumer)`, `possibility(int, Consumer)`
+
+### 4. LalToJavaTranspiler
+`build-tools/precompiler/.../LalToJavaTranspiler.java` (~650 lines)
+
+Groovy AST → Java source transpilation:
+- Statement-based emission with delegation context tracking
+- If/else if/else chains
+- Property access via `getAt()`
+- Cast handling (`as String/Long/Boolean/Integer`)
+- GString interpolation → string concatenation
+- Null-safe navigation (`?.` → ternary null checks)
+- Static method calls (`ProcessRegistry`)
+- Map expression handling (named args)
+- Embedded helper methods (`getAt`, `toLong`, `toInt`, `toBoolean`, `isTruthy`)
+- JavaCompiler batch compilation
+- Manifest writing (`META-INF/lal-expressions.txt`)
+
+### 5. Runtime DSL.java
+Updated `oap-libs-for-graalvm/log-analyzer-for-graalvm/.../dsl/DSL.java`:
+- Loads `LalExpression` from `META-INF/lal-expressions.txt` (not
`DelegatingScript`)
+- `evaluate()` calls `expression.execute(filterSpec, binding)` (not
`script.run()`)
+
+### 6. Dual-Path Tests Updated
+`LALScriptComparisonBase.java` updated:
+- Path A: Fresh Groovy compilation → `DelegatingScript.run()`
+- Path B: Transpiled `LalExpression` from manifest →
`expression.execute(filterSpec, binding)`
+- 19 tests across 5 classes, all passing
+
+---
+
+## Generated Code Example
+
+Input (network-profiling-slow-trace, Groovy):
+```groovy
+filter {
+ json{}
+ extractor{
+ if (tag("LOG_KIND") == "NET_PROFILING_SAMPLED_TRACE") {
+ sampledTrace {
+ latency parsed.latency as Long
+ componentId 49 // simplified
+ }
+ }
+ }
+}
+```
+
+Output (LalExpr_0.java):
+```java
+public class LalExpr_0 implements LalExpression {
+ private static Object getAt(Object obj, String key) { ... }
+ private static long toLong(Object obj) { ... }
+
+ @Override
+ public void execute(FilterSpec filterSpec, Binding binding) {
+ filterSpec.json();
+ filterSpec.extractor(ext -> {
+ if
("NET_PROFILING_SAMPLED_TRACE".equals(filterSpec.tag("LOG_KIND"))) {
+ ext.sampledTrace(st -> {
+ st.latency(toLong(getAt(binding.parsed(), "latency")));
+ st.componentId(49);
+ });
+ }
+ });
+ }
+}
+```
+
+---
+
+## Groovy Runtime Removal
+
+- `groovy-stubs` wired as runtime dependency
+- Real Groovy (`groovy-5.0.3.jar`) moved to test-only scope
+- Native image builds and boots without `org.codehaus.groovy.*` on classpath
+- `GroovyIndyInterfaceFeature` stays dormant (no `org.codehaus.groovy`
packages)
diff --git a/docs/mal-immigration.md b/docs/mal-immigration.md
new file mode 100644
index 0000000..5506e39
--- /dev/null
+++ b/docs/mal-immigration.md
@@ -0,0 +1,817 @@
+# MAL Immigration: Build-Time Pre-Compilation + Groovy Elimination
+
+## 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.
+
+---
+
+## 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);
+ }
+}
+```
+
+---
+
+## Build-Time MAL/LAL Pre-Compilation + MeterSystem Class Generation
+
+**Module**: `build-tools/precompiler` (unified tool)
+
+### MALCompiler.java — main build tool
+
+The tool executes the full initialization pipeline at build time:
+
+#### 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);
+```
+
+#### 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);
+}
+```
+
+#### 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
+}
+```
+
+#### 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`.
+
+**Native image solution**: Dynamic Groovy was eliminated entirely via the
MAL-to-Java transpiler (see below). All MAL expressions are transpiled to pure
Java at build time — no Groovy MOP, no `ExpandoMetaClass`, no `invokedynamic`
at runtime.
+
+### 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.
+
+---
+
+## 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
+```
+
+---
+
+## Config Data Serialization
+
+At build time, the precompiler serializes parsed config POJOs to JSON
manifests in
+`META-INF/config-data/`. This provides the runtime "wiring" data (metric
prefixes,
+rule names, expression lookup keys) that connects pre-compiled Groovy scripts
to
+incoming metrics — without requiring filesystem access to the original YAML
files.
+
+| JSON Manifest | Source Directory | Serialized Type |
+|---|---|---|
+| `meter-analyzer-config.json` | `meter-analyzer-config/` | `Map<String,
MeterConfig>` (filename → config) |
+| `otel-rules.json` | `otel-rules/` | `List<Rule>` |
+| `envoy-metrics-rules.json` | `envoy-metrics-rules/` | `List<Rule>` |
+| `log-mal-rules.json` | `log-mal-rules/` | `List<Rule>` |
+| `telegraf-rules.json` | `telegraf-rules/` | `List<Rule>` |
+| `zabbix-rules.json` | `zabbix-rules/` | `List<Rule>` |
+
+At runtime, replacement loader classes (`MeterConfigs`, `Rules`) deserialize
from
+these JSON files instead of reading YAML from the filesystem. Each logs that
configs
+are loaded from the pre-compiled distro.
+
+---
+
+## Same-FQCN Replacements (MAL)
+
+| Upstream Class | Upstream Location | Replacement Location | What Changed |
+|---|---|---|---|
+| `MeterSystem` | `server-core/.../analysis/meter/MeterSystem.java` |
`oap-libs-for-graalvm/server-core-for-graalvm/` | Complete rewrite. Reads
`@MeterFunction` classes from `META-INF/annotation-scan/MeterFunction.txt`
manifest instead of Guava `ClassPath.from()`. Loads pre-generated Javassist
meter classes from classpath instead of runtime `ClassPool.makeClass()`. |
+| `DSL` (MAL) | `analyzer/meter-analyzer/.../dsl/DSL.java` |
`oap-libs-for-graalvm/meter-analyzer-for-graalvm/` | Complete rewrite. Loads
pre-compiled Groovy `DelegatingScript` classes from
`META-INF/mal-groovy-scripts.txt` manifest instead of `GroovyShell.parse()`
runtime compilation. |
+| `FilterExpression` | `analyzer/meter-analyzer/.../dsl/FilterExpression.java`
| `oap-libs-for-graalvm/meter-analyzer-for-graalvm/` | Complete rewrite. Loads
pre-compiled Groovy filter closure classes from
`META-INF/mal-filter-scripts.properties` manifest instead of
`GroovyShell.evaluate()` runtime compilation. |
+| `Rules` | `analyzer/meter-analyzer/.../prometheus/rule/Rules.java` |
`oap-libs-for-graalvm/meter-analyzer-for-graalvm/` | Complete rewrite. Loads
pre-compiled rule data from `META-INF/config-data/{path}.json` instead of
filesystem YAML files via `ResourceUtils.getPath()` + `Files.walk()`. |
+| `MeterConfigs` |
`analyzer/agent-analyzer/.../meter/config/MeterConfigs.java` |
`oap-libs-for-graalvm/agent-analyzer-for-graalvm/` | Complete rewrite. Loads
pre-compiled meter config data from `META-INF/config-data/{path}.json` instead
of filesystem YAML files via `ResourceUtils.getPathFiles()`. |
+
+All replacements are repackaged into their respective `-for-graalvm` modules
via `maven-shade-plugin` — the original `.class` files are excluded from the
shaded JARs.
+
+---
+
+## Files Created
+
+1. **`build-tools/precompiler/`** (unified, replaces separate `oal-exporter` +
`mal-compiler`)
+ - Main build tool: loads all MAL/LAL rules, runs full initialization
pipeline, exports .class files + manifests
+
+2.
**`oap-libs-for-graalvm/server-core-for-graalvm/src/main/java/.../core/analysis/meter/MeterSystem.java`**
+ - Same-FQCN replacement: reads function registry from manifest, loads
pre-generated classes in `create()`
+
+3.
**`oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/.../meter/analyzer/dsl/DSL.java`**
+ - Same-FQCN replacement: loads pre-compiled MAL Groovy scripts from manifest
+
+4.
**`oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/.../meter/analyzer/dsl/FilterExpression.java`**
+ - Same-FQCN replacement: loads pre-compiled filter closures from manifest
+
+5.
**`oap-graalvm-server/src/test/java/.../graalvm/PrecompiledMALExecutionTest.java`**
+ - Runtime registration and loading tests
+
+6. **`oap-graalvm-server/src/test/java/.../graalvm/mal/`** — 73 comparison
test classes covering all 71 MAL YAML files
+
+## 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/precompiler/target/generated-classes/org/apache/skywalking/oap/server/core/analysis/meter/dynamic/
+
+# 3. Check transpiled Java expression classes exist
+ls
build-tools/precompiler/target/generated-classes/org/apache/skywalking/oap/server/core/source/oal/rt/mal/
+
+# 4. Check manifest files
+wc -l
build-tools/precompiler/target/generated-classes/META-INF/mal-meter-classes.txt
+wc -l
build-tools/precompiler/target/generated-classes/META-INF/mal-expressions.txt
+wc -l
build-tools/precompiler/target/generated-classes/META-INF/lal-expressions.txt
+
+# 5. Check MeterFunction manifest
+cat
build-tools/precompiler/target/generated-classes/META-INF/annotation-scan/MeterFunction.txt
+
+# 6. Verify tests pass
+make build-distro # runs all comparison tests (1300+ assertions)
+```
+
+---
+
+## Pure Java MAL Transpiler
+
+### Why: Groovy + GraalVM Native Image is Incompatible
+
+The first native-image build (`make native-image`) failed with:
+
+```
+Error: Could not find target method:
+ protected static void com.oracle.svm.polyglot.groovy
+
.Target_org_codehaus_groovy_vmplugin_v7_IndyInterface_invalidateSwitchPoints
+ .invalidateSwitchPoints()
+```
+
+**Root cause**: GraalVM's built-in `GroovyIndyInterfaceFeature` (in
`library-support.jar`)
+targets `IndyInterface.invalidateSwitchPoints()` — a method removed in Groovy
5.0.3.
+The Groovy 5.x runtime still uses `org.codehaus.groovy` packages and
`invokedynamic`
+bootstrapping, but the internal API changed. GraalVM's substitution layer
cannot find
+the target method.
+
+**Why fixing Groovy is not worth it**: Even if the substitution were patched,
Groovy's
+dynamic runtime (ExpandoMetaClass, MOP, IndyInterface, MetaClassRegistry)
requires
+extensive GraalVM reflection/substitution configuration. The MAL pre-compiled
scripts
+use `invokedynamic` + Groovy MOP for method resolution. Making this work
reliably in
+native-image would be fragile and complex.
+
+**Solution**: Eliminate Groovy from runtime entirely. Generate pure Java code
at build
+time that implements the same MAL expression logic. The existing 1303 UTs
validate
+that the generated Java code produces identical results to the Groovy-compiled
scripts.
+
+---
+
+### Approach: MAL-to-Java Transpiler
+
+**Key insight**: MAL expressions are method chains on `SampleFamily` with a
well-defined
+API. The precompiler already parses all 1250+ MAL rules. Instead of compiling
them to
+Groovy bytecode (which needs Groovy runtime), we parse the Groovy AST at build
time and
+generate equivalent Java source code. Zero Groovy at runtime.
+
+### What the Transpiler Changes
+
+| Aspect | Before (Groovy pre-compilation) | After (Java transpiler) |
+|--------|----------------------------------|--------------------------|
+| Build output | Groovy `.class` bytecode | Pure Java `.class` files |
+| Runtime dependency | Groovy runtime (MOP, ExpandoMetaClass) | None |
+| Expression execution | `DelegatingScript.run()` + `ExpandoMetaClass` |
`MalExpression.run(Map<String, SampleFamily>)` |
+| Closure parameters | `groovy.lang.Closure` | Java functional interfaces |
+| `propertyMissing()` | Groovy MOP resolves metric names |
`samples.get("metricName")` |
+| `Number * SampleFamily` | `ExpandoMetaClass` on `Number` |
`sf.multiply(number)` (transpiler swaps operands) |
+| Filter expressions | Groovy `Script.run()` → `Closure<Boolean>` |
`SampleFilter.test(Map<String, String>)` |
+
+### What Stays the Same
+
+- **MeterSystem Javassist generation**: Pre-compiled at build time, unchanged
+- **Config data serialization**: JSON manifests for rule configs, unchanged
+- **Manifest-based loading**: Same pattern, new manifest format for Java
expressions
+- **MetricConvert pipeline**: Still runs at build time for Javassist class
generation
+
+---
+
+### Java Functional Interfaces for Closure Replacements
+
+5 `SampleFamily` methods accept `groovy.lang.Closure` parameters. Replace with
Java
+functional interfaces:
+
+| Method | Closure Type | Java Interface |
+|--------|-------------|----------------|
+| `tag(Closure<?> cl)` | `Map<String,String> → Map<String,String>` |
`TagFunction extends Function<Map, Map>` |
+| `filter(Closure<Boolean> f)` | `Map<String,String> → Boolean` |
`SampleFilter extends Predicate<Map>` |
+| `forEach(List, Closure each)` | `(String, Map) → void` | `ForEachFunction
extends BiConsumer<String, Map>` |
+| `decorate(Closure c)` | `MeterEntity → void` | `DecorateFunction extends
Consumer<MeterEntity>` |
+| `instance(..., Closure extractor)` | `Map<String,String> →
Map<String,String>` | `PropertiesExtractor extends Function<Map, Map>` |
+
+**Files created**:
+- `oap-libs-for-graalvm/meter-analyzer-for-graalvm/.../dsl/MalExpression.java`
+-
`oap-libs-for-graalvm/meter-analyzer-for-graalvm/.../dsl/SampleFamilyFunctions.java`
+
+### MalExpression Interface
+
+```java
+public interface MalExpression {
+ SampleFamily run(Map<String, SampleFamily> samples);
+}
+```
+
+Each generated MAL expression class implements this interface. The `samples`
map
+replaces Groovy's `propertyMissing()` delegate — metric names are resolved via
+`samples.get("metricName")`.
+
+### MalFilter Interface
+
+```java
+public interface MalFilter {
+ boolean test(Map<String, String> tags);
+}
+```
+
+Each generated filter expression class implements this interface.
+
+---
+
+### Same-FQCN SampleFamily Replacement
+
+**File**:
`oap-libs-for-graalvm/meter-analyzer-for-graalvm/.../dsl/SampleFamily.java`
+
+Copy upstream `SampleFamily.java` and change ONLY the 5 Closure-based method
signatures:
+
+```java
+// Before (upstream)
+public SampleFamily tag(Closure<?> cl) { ... cl.rehydrate(...).call(arg) ... }
+public SampleFamily filter(Closure<Boolean> filter) { ...
filter.call(it.labels) ... }
+public SampleFamily forEach(List<String> array, Closure<Void> each) { ...
each.call(element, labels) ... }
+public SampleFamily decorate(Closure<Void> c) { ... c.call(meterEntity) ... }
+public SampleFamily instance(..., Closure<Map<String,String>>
propertiesExtractor) { ... }
+
+// After (replacement)
+public SampleFamily tag(TagFunction fn) { ... fn.apply(arg) ... }
+public SampleFamily filter(SampleFilter f) { ... f.test(it.labels) ... }
+public SampleFamily forEach(List<String> array, ForEachFunction each) { ...
each.accept(element, labels) ... }
+public SampleFamily decorate(DecorateFunction fn) { ... fn.accept(meterEntity)
... }
+public SampleFamily instance(..., PropertiesExtractor extractor) { ... }
+```
+
+All other methods (tagEqual, sum, avg, histogram, service, endpoint, etc.)
remain
+identical — they don't use Groovy types.
+
+**Shade config**: Add `SampleFamily.class` and `SampleFamily$*.class` to shade
excludes.
+
+---
+
+### MAL-to-Java Transpiler Implementation
+
+**File**: `build-tools/precompiler/.../MalToJavaTranspiler.java`
+
+Parses MAL expression strings using Groovy's parser (available at build time,
not needed
+at runtime) and generates pure Java source code.
+
+### Input → Output Example
+
+**Input** (MAL expression string from apisix.yaml):
+```
+apisix_http_status.tagNotEqual('route','').tag({tags->tags.route='route/'+tags['route']})
+
.sum(['code','service_name','route','node']).rate('PT1M').endpoint(['service_name'],['route'],
Layer.APISIX)
+```
+
+**Output** (generated Java class):
+```java
+package org.apache.skywalking.oap.server.core.source.oal.rt.mal;
+
+import java.util.*;
+import org.apache.skywalking.oap.meter.analyzer.dsl.*;
+import org.apache.skywalking.oap.server.core.analysis.Layer;
+
+public class MalExpr_meter_apisix_endpoint_http_status implements
MalExpression {
+ @Override
+ public SampleFamily run(Map<String, SampleFamily> samples) {
+ SampleFamily v0 = samples.get("apisix_http_status");
+ if (v0 == null) return SampleFamily.EMPTY;
+ return v0
+ .tagNotEqual("route", "")
+ .tag(tags -> { tags.put("route", "route/" + tags.get("route"));
return tags; })
+ .sum(List.of("code", "service_name", "route", "node"))
+ .rate("PT1M")
+ .endpoint(List.of("service_name"), List.of("route"), Layer.APISIX);
+ }
+}
+```
+
+### AST Node → Java Output Mapping
+
+| Groovy AST Node | Java Output |
+|-----------------|-------------|
+| Bare property access (`metric_name`) | `samples.get("metric_name")` |
+| Method call on SampleFamily | Direct method call (same name) |
+| `Closure` in `.tag()` | Lambda: `tags -> { tags.put(...); return tags; }` |
+| `Closure` in `.filter()` | Lambda: `labels -> booleanExpr` |
+| `Closure` in `.forEach()` | Lambda: `(element, labels) -> { ... }` |
+| `tags.name` inside closure | `tags.get("name")` |
+| `tags.name = val` inside closure | `tags.put("name", val)` |
+| `tags['key']` inside closure | `tags.get("key")` |
+| Binary `SF + SF` | `left.plus(right)` |
+| Binary `SF - SF` | `left.minus(right)` |
+| Binary `SF * SF` | `left.multiply(right)` |
+| Binary `SF / SF` | `left.div(right)` |
+| Binary `Number * SF` | `sf.multiply(number)` (swap operands) |
+| Binary `Number + SF` | `sf.plus(number)` (swap operands) |
+| Binary `Number - SF` | `sf.minus(number).negative()` (swap operands) |
+| Binary `Number / SF` | `sf.newValue(v -> number / v)` (swap operands) |
+| Unary `-SF` | `sf.negative()` |
+| `['a','b','c']` (list literal) | `List.of("a", "b", "c")` |
+| `[50,75,90,95,99]` (int list) | `List.of(50, 75, 90, 95, 99)` |
+| String literal `'foo'` | `"foo"` |
+| Enum constant `Layer.APISIX` | `Layer.APISIX` |
+| Enum constant `DetectPoint.SERVER` | `DetectPoint.SERVER` |
+| `DownsamplingType.LATEST` / `LATEST` | `DownsamplingType.LATEST` |
+| `time()` function | `java.time.Instant.now().getEpochSecond()` |
+| `a?.b` (safe navigation) | `a != null ? a.b : null` |
+| `a ?: b` (elvis) | `a != null ? a : b` |
+
+### Filter Expression Transpilation
+
+Filter expressions are simpler — they are boolean closures:
+
+**Input**: `{ tags -> tags.job_name == 'apisix-monitoring' }`
+
+**Output**:
+```java
+public class MalFilter_apisix implements MalFilter {
+ @Override
+ public boolean test(Map<String, String> tags) {
+ return "apisix-monitoring".equals(tags.get("job_name"));
+ }
+}
+```
+
+---
+
+### Same-FQCN Expression.java Replacement
+
+**File**:
`oap-libs-for-graalvm/meter-analyzer-for-graalvm/.../dsl/Expression.java`
+
+Simplified — no DelegatingScript, no ExpandoMetaClass, no ExpressionDelegate:
+
+```java
+public class Expression {
+ private final String metricName;
+ private final String literal;
+ private final MalExpression expression; // Pure Java
+
+ public Result run(Map<String, SampleFamily> sampleFamilies) {
+ SampleFamily sf = expression.run(sampleFamilies);
+ if (sf == SampleFamily.EMPTY) return Result.fail("EMPTY");
+ return Result.success(sf);
+ }
+
+ // parse() still provides ExpressionParsingContext for static analysis
+ // (scopeType, functionName, etc.) — this is unchanged
+}
+```
+
+---
+
+### Updated DSL.java and FilterExpression.java
+
+### DSL.java (already exists, update)
+
+Change from loading `DelegatingScript` to loading `MalExpression`:
+
+```java
+public static Expression parse(String metricName, String expression) {
+ MalExpression malExpr = loadFromManifest(metricName); // Class.forName +
newInstance
+ return new Expression(metricName, expression, malExpr);
+}
+```
+
+Manifest: `META-INF/mal-expressions.txt` (replaces `mal-groovy-scripts.txt`):
+```
+meter_apisix_endpoint_http_status=org.apache.skywalking.oap.server.core.source.oal.rt.mal.MalExpr_meter_apisix_endpoint_http_status
+```
+
+### FilterExpression.java (already exists, update)
+
+Change from loading Groovy `Script` → `Closure<Boolean>` to loading
`MalFilter`:
+
+```java
+public class FilterExpression {
+ private final MalFilter filter;
+
+ public FilterExpression(String literal) {
+ this.filter = loadFromManifest(literal); // Class.forName +
newInstance
+ }
+
+ public Map<String, SampleFamily> filter(Map<String, SampleFamily>
sampleFamilies) {
+ // Apply filter.test(labels) to each sample family
+ }
+}
+```
+
+---
+
+### Precompiler Integration
+
+Replace `compileMAL()` in `Precompiler.java` to use the transpiler instead of
Groovy
+compilation:
+
+```java
+private static void compileMAL(String outputDir, ...) {
+ MalToJavaTranspiler transpiler = new MalToJavaTranspiler(outputDir);
+
+ for (Rule rule : allRules) {
+ for (MetricsRule mr : rule.getMetricsRules()) {
+ String metricName = formatMetricName(rule, mr.getName());
+ String expression = formatExp(rule.getExpPrefix(),
rule.getExpSuffix(), mr.getExp());
+ transpiler.transpile(metricName, expression);
+ }
+ if (rule.getFilter() != null) {
+ transpiler.transpileFilter(rule.getName(), rule.getFilter());
+ }
+ }
+
+ transpiler.writeManifest(); // mal-expressions.txt
+ transpiler.compileAll(); // javac on generated .java files
+}
+```
+
+The Javassist meter class generation is still run via `MetricConvert` pipeline
(unchanged
+from Phase 2). Only Groovy compilation is replaced by transpilation.
+
+---
+
+### Same-FQCN Replacements (Transpiler)
+
+| Upstream Class | Replacement Location | Change |
+|---|---|---|
+| `SampleFamily` | `meter-analyzer-for-graalvm/` | **New.** Closure parameters
→ functional interfaces |
+| `Expression` | `meter-analyzer-for-graalvm/` | **New.** No Groovy: uses
`MalExpression` instead of `DelegatingScript` |
+| `DSL` (MAL) | `meter-analyzer-for-graalvm/` | **Updated.** Loads
`MalExpression` instead of Groovy `DelegatingScript` |
+| `FilterExpression` | `meter-analyzer-for-graalvm/` | **Updated.** Loads
`MalFilter` instead of Groovy `Closure<Boolean>` |
+
+---
+
+### Files Created (Transpiler)
+
+| File | Purpose |
+|------|---------|
+| `.../dsl/MalExpression.java` | Interface for generated MAL expression
classes |
+| `.../dsl/MalFilter.java` | Interface for generated MAL filter classes |
+| `.../dsl/SampleFamilyFunctions.java` | Functional interfaces: TagFunction,
SampleFilter, ForEachFunction, DecorateFunction, PropertiesExtractor |
+| `.../meter-analyzer-for-graalvm/SampleFamily.java` | Same-FQCN: Closure →
functional interfaces |
+| `.../meter-analyzer-for-graalvm/Expression.java` | Same-FQCN: no Groovy,
uses MalExpression |
+| `.../precompiler/MalToJavaTranspiler.java` | AST-walking transpiler: Groovy
AST → Java source |
+| Generated `MalExpr_*.java` files | ~1250 pure Java expression classes |
+| Generated `MalFilter_*.java` files | ~29 pure Java filter classes |
+
+### Files Modified (Transpiler)
+
+| File | Change |
+|------|--------|
+| `Precompiler.java` | `compileMAL()` uses transpiler instead of Groovy
compilation |
+| `meter-analyzer-for-graalvm/pom.xml` | Add SampleFamily, Expression to shade
excludes |
+| `meter-analyzer-for-graalvm/DSL.java` | Load MalExpression instead of
DelegatingScript |
+| `meter-analyzer-for-graalvm/FilterExpression.java` | Load MalFilter instead
of Groovy Closure |
+
+---
+
+### Transpiler Verification
+
+```bash
+# 1. Rebuild with transpiler
+make compile
+
+# 2. Run ALL existing tests (1303 UTs validate identical MAL behavior)
+make test
+
+# 3. Boot JVM distro
+make shutdown && make boot
+
+# 4. Verify no Groovy at runtime (after LAL is also transpiled)
+jar tf
oap-graalvm-server/target/oap-graalvm-jvm-distro/oap-graalvm-jvm-distro/libs/*.jar
| grep groovy
+# Should find NOTHING
+
+# 5. Attempt native-image build (no more Groovy substitution error)
+make native-image
+```
diff --git a/docs/menu.yml b/docs/menu.yml
index 4b0af8b..a979768 100644
--- a/docs/menu.yml
+++ b/docs/menu.yml
@@ -20,8 +20,14 @@ catalog:
path: "/readme"
- name: "Distribution Policy"
path: "/distro-policy"
- - name: "Configuration"
- path: "/configuration"
+ - name: "Setup"
+ catalog:
+ - name: "Compiling from Source"
+ path: "/compiling"
+ - name: "Pre-Built Docker Images"
+ path: "/docker-image"
+ - name: "Configuration"
+ path: "/configuration"
- name: "Build-Time Immigration"
catalog:
- name: "OAL Pre-Compilation"
diff --git a/docs/oal-immigration.md b/docs/oal-immigration.md
new file mode 100644
index 0000000..dc40f99
--- /dev/null
+++ b/docs/oal-immigration.md
@@ -0,0 +1,162 @@
+# OAL Build-Time Pre-Compilation
+
+## Context
+
+OAL engine generates metrics, builder, and dispatcher classes at runtime via
Javassist (`ClassPool.makeClass()` → `CtClass.toClass()`). GraalVM native image
doesn't support runtime bytecode generation. Additionally, Guava's
`ClassPath.from()` — used by `AnnotationScan.scan()` and
`SourceReceiverImpl.scan()` — doesn't work in native image (no JAR-based
classpath).
+
+**Solution**: Run OAL engine at build time, export `.class` files + manifests.
Replace upstream classes with same-FQCN versions that load from manifests
instead of scanning or generating code.
+
+---
+
+## Build-Time OAL Class Export Tool
+
+**Module**: `build-tools/precompiler` (originally `build-tools/oal-exporter`,
merged into unified precompiler)
+
+**Created**: `OALClassExporter.java` — main class that:
+
+1. Validates all 9 OAL script files are on the classpath
+2. Initializes `DefaultScopeDefine` by scanning `@ScopeDeclaration`
annotations (OAL enricher needs scope metadata)
+3. For each of the 9 `OALDefine` configs: instantiates `OALEngineV2`, enables
debug output (`setOpenEngineDebug(true)` + `setGeneratedFilePath()`), calls
`engine.start()` which parses OAL → enriches → generates `.class` files via
Javassist
+4. Scans the output directory for generated `.class` files and writes OAL
manifests:
+ - `META-INF/oal-metrics-classes.txt` — ~620 fully-qualified class names
+ - `META-INF/oal-dispatcher-classes.txt` — ~45 fully-qualified class names
+ - `META-INF/oal-disabled-sources.txt` — disabled source names from
`disable.oal`
+5. Runs Guava `ClassPath.from()` scan at build time to produce 6
annotation/interface manifests under `META-INF/annotation-scan/`:
+ - `ScopeDeclaration.txt` — classes annotated with `@ScopeDeclaration`
+ - `Stream.txt` — classes annotated with `@Stream` (hardcoded only, not
OAL-generated)
+ - `Disable.txt` — classes annotated with `@Disable`
+ - `MultipleDisable.txt` — classes annotated with `@MultipleDisable`
+ - `SourceDispatcher.txt` — concrete implementations of `SourceDispatcher`
interface (hardcoded only)
+ - `ISourceDecorator.txt` — concrete implementations of `ISourceDecorator`
interface
+
+**Key difference from original plan**: No "collecting listeners" needed.
`engine.start()` generates `.class` files directly to disk via the debug API.
We scan the output directory for class files rather than hooking into engine
callbacks.
+
+### 9 OAL Defines processed
+
+| Define | Config File | Source Package | Catalog |
+|--------|-------------|----------------|---------|
+| `DisableOALDefine` | `oal/disable.oal` | `core.source` | — |
+| `CoreOALDefine` | `oal/core.oal` | `core.source` | — |
+| `JVMOALDefine` | `oal/java-agent.oal` | `core.source` | — |
+| `CLROALDefine` | `oal/dotnet-agent.oal` | `core.source` | — |
+| `BrowserOALDefine` | `oal/browser.oal` | `core.browser.source` | — |
+| `MeshOALDefine` | `oal/mesh.oal` | `core.source` | `ServiceMesh` |
+| `EBPFOALDefine` | `oal/ebpf.oal` | `core.source` | — |
+| `TCPOALDefine` | `oal/tcp.oal` | `core.source` | `EnvoyTCP` |
+| `CiliumOALDefine` | `oal/cilium.oal` | `core.source` | — |
+
+### Generated class packages
+
+- Metrics:
`org.apache.skywalking.oap.server.core.source.oal.rt.metrics.*Metrics`
+- Builders:
`org.apache.skywalking.oap.server.core.source.oal.rt.metrics.builder.*MetricsBuilder`
+- Dispatchers:
`org.apache.skywalking.oap.server.core.source.oal.rt.dispatcher.[catalog].*Dispatcher`
+
+---
+
+## Runtime Registration via Same-FQCN Replacement Classes
+
+Instead of extending upstream classes or hooking via `ModuleWiringBridge`, we
use **same-FQCN replacement**: create classes in `oap-graalvm-server` with the
exact same fully-qualified class name as the upstream class. Maven classpath
precedence ensures our version is loaded instead of the upstream version.
+
+### 3 replacement classes created:
+
+**1. `OALEngineLoaderService`**
(`oap-graalvm-server/.../core/oal/rt/OALEngineLoaderService.java`)
+
+Same FQCN as upstream
`org.apache.skywalking.oap.server.core.oal.rt.OALEngineLoaderService`. On first
`load()` call:
+- Reads `META-INF/oal-disabled-sources.txt` → registers with `DisableRegister`
+- Reads `META-INF/oal-metrics-classes.txt` → `Class.forName()` →
`StreamAnnotationListener.notify()`
+- Reads `META-INF/oal-dispatcher-classes.txt` → `Class.forName()` →
`DispatcherDetectorListener.addIfAsSourceDispatcher()`
+- All subsequent `load()` calls are no-ops (all classes registered on first
call regardless of which `OALDefine` triggered it)
+
+**2. `AnnotationScan`**
(`oap-graalvm-server/.../core/annotation/AnnotationScan.java`)
+
+Same FQCN as upstream
`org.apache.skywalking.oap.server.core.annotation.AnnotationScan`. Instead of
Guava `ClassPath.from()` scanning, reads manifest files from
`META-INF/annotation-scan/{AnnotationSimpleName}.txt`. Each registered
`AnnotationListener` is matched against its corresponding manifest.
+
+**3. `SourceReceiverImpl`**
(`oap-graalvm-server/.../core/source/SourceReceiverImpl.java`)
+
+Same FQCN as upstream
`org.apache.skywalking.oap.server.core.source.SourceReceiverImpl`. `scan()`
reads from `META-INF/annotation-scan/SourceDispatcher.txt` and
`META-INF/annotation-scan/ISourceDecorator.txt` instead of Guava classpath
scanning.
+
+### Key differences from original plan:
+- **No extending** — same-FQCN replacement instead of subclassing
+- **No `ModuleWiringBridge` changes** — classpath precedence handles the swap
automatically
+- **3 replacement classes, not 1** — `AnnotationScan` and `SourceReceiverImpl`
also needed replacement
+- **Classpath scanning fully eliminated** — annotation manifests replace Guava
scanning
+
+---
+
+## Class Loading and Remaining Scans
+
+### `Class.forName()` in native image
+`Class.forName()` is supported in GraalVM native image when classes are
registered in `reflect-config.json`. Since all pre-generated classes are on the
classpath at native-image build time, the GraalVM compiler includes them in the
binary. The `reflect-config.json` entries enable runtime `Class.forName()`
lookup.
+
+### OAL-internal scans — build-time only
+The 3 OAL-internal scans (`MetricsHolder`, `DefaultMetricsFunctionRegistry`,
`FilterMatchers`) only run inside the OAL engine during `engine.start()`. They
happen at **build time** in `OALClassExporter`, not at runtime. Automatically
solved.
+
+### `MeterSystem` — solved in MAL immigration
+`MeterSystem` uses Guava `ClassPath.from()` to discover meter function classes
at runtime. Replaced with manifest-based loading. See
[mal-immigration.md](mal-immigration.md).
+
+### `reflect-config.json`
+GraalVM reflection configuration for `Class.forName()` calls on OAL-generated
and manifest-listed classes is auto-generated by the precompiler from manifests.
+
+---
+
+## Same-FQCN Replacements (OAL)
+
+| Upstream Class | Upstream Location | Replacement Location | What Changed |
+|---|---|---|---|
+| `OALEngineLoaderService` |
`server-core/.../oal/rt/OALEngineLoaderService.java` |
`oap-libs-for-graalvm/server-core-for-graalvm/` | Complete rewrite. Loads
pre-compiled OAL classes from build-time manifests instead of running ANTLR4 +
FreeMarker + Javassist at runtime. |
+| `AnnotationScan` | `server-core/.../annotation/AnnotationScan.java` |
`oap-libs-for-graalvm/server-core-for-graalvm/` | Complete rewrite. Reads
`META-INF/annotation-scan/{name}.txt` manifests instead of Guava
`ClassPath.from()` scanning. |
+| `SourceReceiverImpl` | `server-core/.../source/SourceReceiverImpl.java` |
`oap-libs-for-graalvm/server-core-for-graalvm/` | Complete rewrite. Reads
dispatcher/decorator manifests instead of Guava `ClassPath.from()` scanning. |
+
+All three replacements are repackaged into `server-core-for-graalvm` via
`maven-shade-plugin` — the original `.class` files are excluded from the shaded
JAR.
+
+---
+
+## Files Created
+
+1. **`build-tools/precompiler/src/main/java/.../Precompiler.java`** (unified,
originally `oal-exporter`)
+ - Build-time tool: runs 9 OAL defines, exports `.class` files, writes OAL
manifests + annotation/interface manifests + reflection metadata
+
+2.
**`oap-libs-for-graalvm/server-core-for-graalvm/src/main/java/.../core/oal/rt/OALEngineLoaderService.java`**
+ - Same-FQCN replacement: loads pre-compiled OAL classes from manifests
+
+3.
**`oap-libs-for-graalvm/server-core-for-graalvm/src/main/java/.../core/annotation/AnnotationScan.java`**
+ - Same-FQCN replacement: reads annotation manifests instead of Guava
classpath scanning
+
+4.
**`oap-libs-for-graalvm/server-core-for-graalvm/src/main/java/.../core/source/SourceReceiverImpl.java`**
+ - Same-FQCN replacement: reads dispatcher/decorator manifests instead of
Guava classpath scanning
+
+5. **`oap-graalvm-server/src/test/java/.../PrecompiledRegistrationTest.java`**
+ - 12 tests: manifest vs Guava scan comparison, OAL class loading, scope
registration, source→dispatcher→metrics chain consistency
+
+## Key Upstream Files (read-only)
+
+- `OALEngineV2.java` — `start()` (parse → enrich → generate),
`notifyAllListeners()` (register)
+- `OALClassGeneratorV2.java` — `setOpenEngineDebug(true)`,
`setGeneratedFilePath()`, `writeGeneratedFile()` exports via
`ctClass.toBytecode()`
+- `OALEngineLoaderService.java` (upstream) — `load()` creates engine, sets
listeners, calls `start()`+`notifyAllListeners()`
+- `StorageBuilderFactory.java:67-78` — `Default` impl uses `metrics-builder`
template path
+- `StreamAnnotationListener.java` — `notify(Class)` reads `@Stream`, routes to
`MetricsStreamProcessor.create()`
+- `CoreModuleProvider.java:356-357` — registers `OALEngineLoaderService` in
`prepare()`
+- `CoreModuleProvider.java:417-421` — `start()` calls `load(DisableOALDefine)`
then `scan()`
+
+---
+
+## Verification
+
+```bash
+# 1. Build everything
+make build-distro
+
+# 2. Check generated classes exist
+ls
build-tools/precompiler/target/generated-classes/org/apache/skywalking/oap/server/core/source/oal/rt/metrics/
+ls
build-tools/precompiler/target/generated-classes/org/apache/skywalking/oap/server/core/source/oal/rt/dispatcher/
+
+# 3. Check manifest files
+cat
build-tools/precompiler/target/generated-classes/META-INF/oal-metrics-classes.txt
+cat
build-tools/precompiler/target/generated-classes/META-INF/oal-dispatcher-classes.txt
+
+# 4. Check annotation scan manifests
+ls build-tools/precompiler/target/generated-classes/META-INF/annotation-scan/
+
+# 5. Verify tests pass
+make build-distro # runs PrecompiledRegistrationTest
+```