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 bfa96dc Add LAL comparison tests, documentation overhaul, packaging
strategy
bfa96dc is described below
commit bfa96dcb1d8e74a0e55d403a5f19eedbd2a290c9
Author: Wu Sheng <[email protected]>
AuthorDate: Thu Feb 19 20:54:50 2026 +0800
Add LAL comparison tests, documentation overhaul, packaging strategy
- Add 19 LAL comparison tests across 5 test classes covering all 8 YAML
files and 10 rules with full branch coverage
- Create LAL-IMMIGRATION.md documenting LAL pre-compilation approach
- Rename PLAN.md to DISTRO-POLICY.md, add same-FQCN packaging strategy
(Challenge 5) with maven-shade-plugin filter plan for Phase 3
- Simplify README.md to high-level intro, link to immigration docs
- Add same-FQCN override explanation comment in oap-graalvm-server pom.xml
- Update cluster selection to Standalone + Kubernetes across all docs
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
CLAUDE.md | 17 +-
PLAN.md => DISTRO-POLICY.md | 86 ++-
LAL-IMMIGRATION.md | 192 +++++++
README.md | 137 +----
oap-graalvm-server/pom.xml | 27 +
.../oap/server/graalvm/lal/LALDefaultTest.java | 65 +++
.../oap/server/graalvm/lal/LALEnvoyAlsTest.java | 138 +++++
.../graalvm/lal/LALNetworkProfilingTest.java | 279 ++++++++++
.../oap/server/graalvm/lal/LALNginxTest.java | 145 +++++
.../graalvm/lal/LALScriptComparisonBase.java | 593 +++++++++++++++++++++
.../oap/server/graalvm/lal/LALSlowSqlTest.java | 162 ++++++
11 files changed, 1710 insertions(+), 131 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index e7e1ab5..163450b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,13 +4,13 @@
- `skywalking/` — Git submodule of `apache/skywalking.git`. **Do not modify
directly.** All SkyWalking source changes go through upstream PRs.
- `build-tools/precompiler/` — Build-time precompiler: runs OAL + MAL + LAL
engines at Maven compile time, exports `.class` files and manifests into
`precompiler-*-generated.jar`.
- `oap-graalvm-server/` — GraalVM-ready OAP server module with same-FQCN
replacement classes and comprehensive test suites.
-- `PLAN.md` — Current build plan (draft/temporary, subject to change).
+- `DISTRO-POLICY.md` — Current build plan (draft/temporary, subject to change).
- Root-level Maven + Makefile — Orchestrates building on top of the submodule.
## Key Principles
1. **Minimize upstream changes.** SkyWalking is a submodule. Changes to it
require separate upstream PRs and syncing back.
2. **Build-time class export.** All runtime code generation (OAL via
Javassist, MAL/LAL via Groovy) runs at build time. Export `.class` files into
native-image classpath.
-3. **Fixed module wiring.** Module/provider selection is hardcoded in this
distro — no SPI discovery. See PLAN.md for the full module table.
+3. **Fixed module wiring.** Module/provider selection is hardcoded in this
distro — no SPI discovery. See DISTRO-POLICY.md for the full module table.
4. **JDK 25.** Already compiles and runs.
## Technical Notes
@@ -21,12 +21,11 @@
- **Classpath scanning**: Guava `ClassPath.from()` used in multiple places.
Run at build-time pre-compilation as verification gate, export static class
index.
- **Config loading**: `YamlConfigLoaderUtils.copyProperties()` uses
`Field.setAccessible()` + `field.set()`. Register known `ModuleConfig` classes
for GraalVM reflection.
-## MAL Test Suite
-71 MAL YAML files are covered by comparison tests (1281 test assertions). Each
test runs every metric expression through two independent paths:
-- **Path A**: Fresh GroovyShell compilation (runtime behavior)
-- **Path B**: Pre-compiled class from build-time JAR
+## Test Suites
+- **MAL**: 71 YAML files covered by 73 test classes (1,281 assertions). See
`oap-graalvm-server/src/test/CLAUDE.md` for test generation instructions and
`oap-graalvm-server/src/test/MAL-COVERAGE.md` for coverage tracking.
+- **LAL**: 8 YAML files covered by 5 test classes (19 assertions). See
[LAL-IMMIGRATION.md](LAL-IMMIGRATION.md) for details.
-Both paths must produce identical results. See
`oap-graalvm-server/src/test/CLAUDE.md` for test generation instructions and
`oap-graalvm-server/src/test/MAL-COVERAGE.md` for detailed coverage tracking.
+Both suites use dual-path comparison: Path A (fresh GroovyShell compilation)
vs Path B (pre-compiled class from build-time JAR). Both paths must produce
identical results.
## Build Commands
```bash
@@ -42,6 +41,6 @@ JAVA_HOME=/Users/wusheng/.sdkman/candidates/java/25-graal mvn
-pl oap-graalvm-se
## Selected Modules
- **Storage**: BanyanDB
-- **Cluster**: Kubernetes
+- **Cluster**: Standalone, Kubernetes
- **Configuration**: Kubernetes
-- **Receivers/Query/Analyzers/Alarm/Telemetry/Other**: Full feature set (see
PLAN.md for details)
\ No newline at end of file
+- **Receivers/Query/Analyzers/Alarm/Telemetry/Other**: Full feature set (see
DISTRO-POLICY.md for details)
\ No newline at end of file
diff --git a/PLAN.md b/DISTRO-POLICY.md
similarity index 70%
rename from PLAN.md
rename to DISTRO-POLICY.md
index 2f36c6a..ebf0d34 100644
--- a/PLAN.md
+++ b/DISTRO-POLICY.md
@@ -1,4 +1,4 @@
-# SkyWalking GraalVM Distro - Build Plan
+# SkyWalking GraalVM Distro - Distribution Policy
## Goal
Build and package Apache SkyWalking OAP server as a GraalVM native image on
JDK 25.
@@ -14,7 +14,7 @@ Build and package Apache SkyWalking OAP server as a GraalVM
native image on JDK
|----------|--------|----------|
| **Core** | CoreModule | default |
| **Storage** | StorageModule | BanyanDB |
-| **Cluster** | ClusterModule | Kubernetes |
+| **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 |
@@ -43,7 +43,9 @@ Build and package Apache SkyWalking OAP server as a GraalVM
native image on JDK
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. See
[OAL-IMMIGRATION.md](OAL-IMMIGRATION.md) for details.
+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
@@ -64,6 +66,8 @@ All `.oal` scripts are known. Run OAL engine at build time,
export `.class` file
### Approach (this repo)
Run full MAL/LAL initialization at build time via `build-tools/precompiler`
(unified tool). Export Javassist-generated `.class` files + compiled Groovy
script bytecode. At runtime, load from manifests.
+**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 and 1250 Groovy
scripts.
- **Manifests**: `META-INF/mal-groovy-manifest.txt` (script→class mapping),
`META-INF/mal-groovy-expression-hashes.txt` (SHA-256 for combination pattern
resolution), `META-INF/mal-meter-classes.txt` (Javassist-generated classes),
`META-INF/annotation-scan/MeterFunction.txt` (16 function classes).
@@ -110,7 +114,74 @@ MAL expressions rely on `propertyMissing()` for sample
name resolution and `Expa
---
-## Challenge 5: Additional GraalVM Risks
+## Challenge 5: Same-FQCN Packaging for Distribution
+
+### Problem
+
+The same-FQCN replacement technique works during Maven compilation and testing
because Maven places `target/classes/` (the module's own compiled source) on
the classpath **before** all dependency JARs. Java's classloader uses
first-found-wins, so our replacement classes shadow the upstream originals.
+
+However, this implicit ordering does **not** carry over to distribution
packaging:
+
+- **Uber JAR** (maven-shade-plugin): When merging all JARs into one, duplicate
FQCN class files collide. Only one copy survives, and the winner depends on
dependency processing order — not guaranteed to be ours.
+- **Classpath mode** (separate JARs): `java -cp jar1:jar2:jar3` or
`native-image -cp jar1:jar2:jar3` uses first-found-wins based on `-cp`
ordering. Requires explicit ordering that is fragile and easy to break.
+- **GraalVM native-image**: Uses the same classpath resolution as JVM.
Duplicate FQCNs on the classpath may produce warnings or unpredictable behavior.
+
+### 7 Same-FQCN Replacement Classes
+
+| Replacement Class | Upstream Artifact | Purpose |
+|---|---|---|
+| `OALEngineLoaderService` | `server-core` | Load OAL classes from manifests
instead of Javassist |
+| `AnnotationScan` | `server-core` | Read annotation manifests instead of
Guava ClassPath |
+| `SourceReceiverImpl` | `server-core` | Read dispatcher/decorator manifests
instead of Guava ClassPath |
+| `MeterSystem` | `server-core` | Read MeterFunction manifest + pre-generated
meter classes |
+| `DSL` (MAL) | `meter-analyzer` | Load pre-compiled MAL Groovy scripts from
manifest |
+| `FilterExpression` | `meter-analyzer` | Load pre-compiled filter closures
from manifest |
+| `DSL` (LAL) | `log-analyzer` | Load pre-compiled LAL scripts via SHA-256
hash lookup |
+
+### Approach: Uber JAR with Explicit Shade Filters
+
+The `oap-graalvm-native` module will use `maven-shade-plugin` to produce a
single uber JAR with explicit filters that exclude the 7 upstream classes. This
makes conflict resolution explicit and auditable.
+
+```xml
+<plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <configuration>
+ <filters>
+ <filter>
+ <artifact>org.apache.skywalking:server-core</artifact>
+ <excludes>
+
<exclude>org/apache/skywalking/oap/server/core/oal/rt/OALEngineLoaderService.class</exclude>
+
<exclude>org/apache/skywalking/oap/server/core/annotation/AnnotationScan.class</exclude>
+
<exclude>org/apache/skywalking/oap/server/core/source/SourceReceiverImpl.class</exclude>
+
<exclude>org/apache/skywalking/oap/server/core/analysis/meter/MeterSystem.class</exclude>
+ </excludes>
+ </filter>
+ <filter>
+ <artifact>org.apache.skywalking:meter-analyzer</artifact>
+ <excludes>
+
<exclude>org/apache/skywalking/oap/meter/analyzer/dsl/DSL.class</exclude>
+
<exclude>org/apache/skywalking/oap/meter/analyzer/dsl/FilterExpression.class</exclude>
+ </excludes>
+ </filter>
+ <filter>
+ <artifact>org.apache.skywalking:log-analyzer</artifact>
+ <excludes>
+
<exclude>org/apache/skywalking/oap/log/analyzer/dsl/DSL.class</exclude>
+ </excludes>
+ </filter>
+ </filters>
+ </configuration>
+</plugin>
+```
+
+The resulting uber JAR contains exactly one copy of each class — no
duplicates, no ordering ambiguity. This JAR is then fed to `native-image` for
AOT compilation.
+
+**Important**: When new same-FQCN replacement classes are added, the shade
filter list must be updated to match. This list should be kept in sync with the
table above.
+
+---
+
+## Challenge 6: Additional GraalVM Risks
| Risk | Mitigation |
|------|------------|
@@ -150,12 +221,17 @@ MAL expressions rely on `propertyMissing()` for sample
name resolution and `Expa
- [x] SHA-256 staleness detection for submodule YAML drift
- [x] `MeterSystem` classpath scan eliminated via manifest
+**LAL immigration — COMPLETE:**
+- [x] LAL Groovy pre-compilation: 8 YAML files → 10 rules → 6 unique
pre-compiled classes (`@CompileStatic`)
+- [x] Same-FQCN replacement: `DSL.java` (LAL) loads pre-compiled scripts via
SHA-256 hash lookup
+- [x] Comparison test suite: 5 test classes, 19 assertions (all 8 YAML files,
100% branch coverage)
+
**Remaining:**
-- [ ] LAL Groovy pre-compilation (LAL `DSL.java` replacement exists but
runtime behavior not yet tested — LAL is lower priority since it has only 10
rules)
- [ ] Config generator (`build-tools/config-generator` skeleton exists) —
eliminate `Field.setAccessible` reflection in config loading
- [ ] Package everything into native-image classpath
### Phase 3: Native Image Build
+- [ ] Uber JAR packaging via `maven-shade-plugin` in `oap-graalvm-native` with
explicit shade filters for same-FQCN classes (see Challenge 5)
- [ ] `native-image-maven-plugin` configuration
- [ ] Run tracing agent to capture reflection/resource/JNI metadata
- [ ] Configure gRPC/Netty/Protobuf for native image
diff --git a/LAL-IMMIGRATION.md b/LAL-IMMIGRATION.md
new file mode 100644
index 0000000..0e671dc
--- /dev/null
+++ b/LAL-IMMIGRATION.md
@@ -0,0 +1,192 @@
+# Phase 2: LAL Build-Time Pre-Compilation — COMPLETE
+
+## 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 two manifest files:
+
+**`META-INF/lal-scripts-by-hash.txt`** — SHA-256 hash → class name:
+```
+a1b2c3d4...=org.apache.skywalking.oap.server.core.analysis.lal.script.LALScript0001
+e5f6a7b8...=org.apache.skywalking.oap.server.core.analysis.lal.script.LALScript0002
+...
+```
+
+**`META-INF/lal-scripts.txt`** — rule name → class name:
+```
+default=org.apache.skywalking.oap.server.core.analysis.lal.script.LALScript0001
+nginx-access-log=org.apache.skywalking.oap.server.core.analysis.lal.script.LALScript0002
+...
+```
+
+The hash-based manifest enables runtime lookup by DSL content (independent of
rule naming), while the name-based manifest enables verification that all
expected rules are covered.
+
+---
+
+## 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
+
+---
+
+## Files Created
+
+1. **`oap-graalvm-server/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.
diff --git a/README.md b/README.md
index 5598c7a..993eea6 100644
--- a/README.md
+++ b/README.md
@@ -1,121 +1,31 @@
# SkyWalking GraalVM Distro (Experimental)
<img src="http://skywalking.apache.org/assets/logo.svg" alt="Sky Walking logo"
height="90px" align="right" />
-GraalVM compiles your Java applications ahead of time into standalone
binaries. These binaries are smaller, start up to 100x faster, provide peak
performance with no warmup, and use less memory and CPU than applications
running on a Java Virtual Machine (JVM).
+SkyWalking GraalVM Distro is a re-distribution of the official Apache
SkyWalking OAP server, targeting GraalVM native image on JDK 25.
-SkyWalking GraalVM Distro is a re-distribution version of the official Apache
SkyWalking OAP server, targeting GraalVM native image on JDK 25.
+## Why a Re-Distribution?
-## Why This Is Hard
-
-SkyWalking OAP server relies heavily on runtime code generation and dynamic
class loading — patterns that are fundamentally incompatible with GraalVM
native image's closed-world assumption:
-
-| Runtime Pattern | Where Used | Scale |
-|---|---|---|
-| **Javassist bytecode generation** | OAL metrics classes, MAL meter classes |
~1,850 classes generated at startup |
-| **Groovy dynamic compilation** | MAL meter expressions, LAL log rules |
~1,260 scripts compiled at startup |
-| **Guava ClassPath scanning** | Annotation discovery, function registry | 7
scanning sites |
-| **ServiceLoader (SPI)** | Module/provider discovery | All modules |
-| **Reflection-based config** | `Field.setAccessible()` for YAML config | All
`ModuleConfig` subclasses |
+SkyWalking OAP server relies heavily on runtime code generation and dynamic
class loading — patterns that are fundamentally incompatible with GraalVM
native image's closed-world assumption. This includes Javassist bytecode
generation (~1,850 classes), Groovy dynamic compilation (~1,260 scripts), Guava
classpath scanning, ServiceLoader discovery, and reflection-based configuration.
None of these work in a GraalVM native image out of the box.
-## Strategy: Build-Time Pre-Compilation
-
-The core idea is simple: **move all dynamic code generation and classpath
scanning from runtime to build time**. At build time, run the full OAL/MAL/LAL
initialization pipeline, capture all generated bytecode, and package it into
the native image classpath. At runtime, load pre-compiled classes from
manifests — no Javassist, no GroovyShell, no ClassPath scanning.
-
-```
-┌─────────────────────────────────────────────────────────┐
-│ BUILD TIME │
-│ │
-│ skywalking/ build-tools/precompiler │
-│ (submodule) ┌──────────────────────────────┐ │
-│ │ │ 1. Run OAL engine (Javassist)│ │
-│ │ │ → 620 metrics classes │ │
-│ │ │ → 620 builder classes │ │
-│ │ │ → 45 dispatcher classes │ │
-│ │ │ │ │
-│ │ │ 2. Run MAL engine (Groovy) │ │
-│ │ │ → 1250 Groovy scripts │ │
-│ │ │ → 1209 meter classes │ │
-│ │ │ │ │
-│ │ │ 3. Classpath scan │ │
-│ │ │ → 7 annotation manifests │ │
-│ │ │ │ │
-│ │ │ 4. Write manifests │ │
-│ │ └──────────┬───────────────────┘ │
-│ │ │
-│ ▼ │
-│ precompiler-generated.jar │
-│ (all .class files + manifests) │
-└─────────────────────────────────────────────────────────┘
- │
- ▼
-┌─────────────────────────────────────────────────────────┐
-│ RUNTIME │
-│ │
-│ oap-graalvm-server │
-│ ┌─────────────────────────────────────────────┐ │
-│ │ Same-FQCN replacement classes: │ │
-│ │ │ │
-│ │ OALEngineLoaderService │ │
-│ │ → reads oal-metrics-classes.txt manifest │ │
-│ │ → Class.forName() + register │ │
-│ │ │ │
-│ │ DSL.java (MAL) │ │
-│ │ → reads mal-groovy-manifest.txt │ │
-│ │ → Class.forName() pre-compiled scripts │ │
-│ │ │ │
-│ │ MeterSystem.java │ │
-│ │ → reads MeterFunction.txt manifest │ │
-│ │ → reads mal-meter-classes.txt manifest │ │
-│ │ → no Javassist, no ClassPath.from() │ │
-│ │ │ │
-│ │ AnnotationScan.java │ │
-│ │ → reads annotation-scan/*.txt manifests │ │
-│ │ → no Guava ClassPath.from() │ │
-│ └─────────────────────────────────────────────┘ │
-└─────────────────────────────────────────────────────────┘
-```
-
-The **same-FQCN replacement** technique makes this transparent: classes in
`oap-graalvm-server` share the exact fully-qualified name with their upstream
counterparts. Maven classpath ordering ensures the replacement is loaded. No
forking, no patching, no SPI trickery.
+This distro moves all dynamic code generation from runtime to build time. At
build time, the full OAL/MAL/LAL initialization pipeline runs, captures all
generated bytecode, and packages it into the native image classpath. At
runtime, pre-compiled classes are loaded from manifests — no Javassist, no
GroovyShell, no classpath scanning.
-## Current Status
-
-### Phase 1: Build System Setup — COMPLETE
-- Maven + Makefile build orchestration
-- SkyWalking as git submodule (`skywalking/`)
-- Fixed module manager (no SPI discovery)
-- Simplified `application.yml` for selected providers
-
-### Phase 2: Build-Time Pre-Compilation — COMPLETE
-- **OAL immigration**: 9 OAL defines → ~1,285 Javassist classes exported, 6
annotation manifests
-- **MAL immigration**: 71 MAL YAML files → 1,250 Groovy scripts + 1,209
Javassist meter classes exported
-- **Classpath scanning eliminated**: All 7 Guava `ClassPath.from()` sites
replaced with build-time manifests
-- **Verification**: 1,281 comparison tests validate pre-compiled classes match
fresh Groovy compilation
-
-### Phase 3: Native Image Build — NOT STARTED
-- `native-image-maven-plugin` configuration
-- GraalVM reflection/resource/JNI metadata (`reflect-config.json`,
`resource-config.json`)
-- gRPC/Netty/Protobuf native image configuration
-- Dynamic Groovy MOP compatibility (biggest unknown risk)
-- Get OAP server booting as native image with BanyanDB storage
-
-### Phase 4: Harden & Test — NOT STARTED
-- Verify all receiver plugins, query APIs, cluster mode, alarm
-- Performance benchmarking vs JVM
-- CI: automated native-image build + smoke tests
+The **same-FQCN replacement** technique makes this transparent: classes in
this distro share the exact fully-qualified name with their upstream
counterparts. Maven classpath ordering ensures the replacement is loaded. No
forking, no patching.
## Module Selection
This distro targets a **full-feature OAP server** with fixed module/provider
selection:
- **Storage**: BanyanDB
-- **Cluster**: Kubernetes
+- **Cluster**: Standalone, Kubernetes
- **Configuration**: Kubernetes
- **Receivers**: All (trace, meter, log, profile, browser, OTel, mesh, envoy,
Zipkin, Zabbix, Telegraf, etc.)
- **Query**: GraphQL, PromQL, LogQL, Zipkin
- **Alarm, Telemetry, Exporter**: Enabled
+See [DISTRO-POLICY.md](DISTRO-POLICY.md) for the full module table and build
plan.
+
## Build
Requires GraalVM JDK 25.
@@ -139,28 +49,21 @@ skywalking-graalvm-distro/
├── oap-graalvm-server/
│ └── src/
│ ├── main/java/ # Same-FQCN replacement classes
-│ │ ├── .../oal/rt/ # OALEngineLoaderService
-│ │ ├── .../annotation/ # AnnotationScan
-│ │ ├── .../source/ # SourceReceiverImpl
-│ │ ├── .../meter/ # MeterSystem, DSL, FilterExpression
-│ │ └── .../log/ # LAL DSL
-│ └── test/java/ # 1,281 comparison tests
-├── PLAN.md # Detailed build plan with phase tracking
-├── OAL-IMMIGRATION.md # OAL pre-compilation design doc
-└── MAL-IMMIGRATION.md # MAL pre-compilation design doc
+│ └── test/java/ # 1,300 comparison tests
+├── DISTRO-POLICY.md # Build plan with phase tracking
+├── OAL-IMMIGRATION.md # OAL pre-compilation design
+├── MAL-IMMIGRATION.md # MAL pre-compilation design
+└── LAL-IMMIGRATION.md # LAL pre-compilation design
```
-## Known Risks
-
-| Risk | Severity | Mitigation |
-|---|---|---|
-| Dynamic Groovy MOP in native image | High | `ExpandoMetaClass` +
`propertyMissing()` may not work. Fallback: upstream DSL changes to eliminate
dynamic dispatch. |
-| gRPC / Netty native image | Medium | GraalVM reachability metadata repo,
Netty substitutions |
-| Reflection sites beyond config | Medium | Tracing agent +
`reflect-config.json` |
-| Kubernetes client in native image | Low | Has documented GraalVM support |
+## Documentation
-## Release Policy
-This repository is experimental. No releases are planned until a working
native image is achieved.
+| Document | Description |
+|---|---|
+| [DISTRO-POLICY.md](DISTRO-POLICY.md) | Build plan, module selection, phase
tracking, risk assessment |
+| [OAL-IMMIGRATION.md](OAL-IMMIGRATION.md) | OAL (Observability Analysis
Language) pre-compilation: Javassist class export, annotation scanning, runtime
manifests |
+| [MAL-IMMIGRATION.md](MAL-IMMIGRATION.md) | MAL (Meter Analysis Language)
pre-compilation: Groovy script compilation, Javassist meter classes,
combination pattern |
+| [LAL-IMMIGRATION.md](LAL-IMMIGRATION.md) | LAL (Log Analysis Language)
pre-compilation: static Groovy compilation, SHA-256 manifest lookup |
## License
Apache 2.0
diff --git a/oap-graalvm-server/pom.xml b/oap-graalvm-server/pom.xml
index a336de7..b0c7c83 100644
--- a/oap-graalvm-server/pom.xml
+++ b/oap-graalvm-server/pom.xml
@@ -32,6 +32,30 @@
<description>SkyWalking OAP server with fixed module wiring for GraalVM
(JVM mode)</description>
<dependencies>
+ <!--
+ == Same-FQCN Override ==
+ This module contains 7 classes that share the exact fully-qualified
class name
+ with classes in upstream dependencies (server-core, meter-analyzer,
log-analyzer).
+ These replacement classes load pre-compiled OAL/MAL/LAL assets from
build-time
+ manifests instead of using runtime code generation (Javassist,
Groovy, Guava ClassPath).
+
+ WHY IT WORKS NOW (compile + test):
+ Maven places this module's compiled classes (target/classes/) on the
classpath
+ BEFORE all dependency JARs. Java's classloader uses
first-found-wins, so our
+ replacement classes shadow the upstream originals during compilation
and testing.
+
+ WHAT WE WILL DO FOR FINAL PACKAGING (Phase 3):
+ The oap-graalvm-native module will use maven-shade-plugin to produce
an uber JAR
+ with explicit <filters> that exclude the 7 upstream classes from
their origin
+ artifacts. This eliminates duplicate FQCNs entirely, making the
conflict resolution
+ explicit and safe for GraalVM native-image. See DISTRO-POLICY.md
Challenge 5.
+
+ Shadowed classes:
+ server-core -> OALEngineLoaderService, AnnotationScan,
SourceReceiverImpl, MeterSystem
+ meter-analyzer -> DSL (MAL), FilterExpression
+ log-analyzer -> DSL (LAL)
+ -->
+
<!-- Core -->
<dependency>
<groupId>org.apache.skywalking</groupId>
@@ -293,6 +317,9 @@
<argLine>
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.lang.reflect=ALL-UNNAMED
+ --add-opens java.base/java.util=ALL-UNNAMED
+ --add-opens java.base/java.lang.invoke=ALL-UNNAMED
+ -Dnet.bytebuddy.experimental=true
</argLine>
</configuration>
</plugin>
diff --git
a/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/lal/LALDefaultTest.java
b/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/lal/LALDefaultTest.java
new file mode 100644
index 0000000..35657ea
--- /dev/null
+++
b/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/lal/LALDefaultTest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.server.graalvm.lal;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.apache.skywalking.apm.network.logging.v3.LogData;
+import org.apache.skywalking.oap.log.analyzer.provider.LALConfig;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Comparison test for lal/default.yaml.
+ *
+ * <p>The simplest LAL rule — just saves all logs with no parsing or
extraction:
+ * <pre>
+ * filter {
+ * sink {
+ * }
+ * }
+ * </pre>
+ */
+class LALDefaultTest extends LALScriptComparisonBase {
+
+ @Test
+ void defaultRuleSavesAllLogs() throws Exception {
+ final List<LALConfig> rules = loadLALRules("default.yaml");
+ assertEquals(1, rules.size());
+ assertEquals("default", rules.get(0).getName());
+
+ final LogData logData = buildTextLogData(
+ "test-service", "test-instance", "some log message",
+ Collections.emptyMap());
+
+ runAndCompare(rules.get(0).getDsl(), logData);
+ }
+
+ @Test
+ void manifestContainsDefaultRule() {
+ final Map<String, String> nameManifest = loadNameManifest();
+ assertTrue(nameManifest.containsKey("default"),
+ "lal-scripts.txt should contain 'default' rule");
+ assertFalse(nameManifest.get("default").isEmpty(),
+ "default rule should map to a class name");
+ }
+}
diff --git
a/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/lal/LALEnvoyAlsTest.java
b/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/lal/LALEnvoyAlsTest.java
new file mode 100644
index 0000000..0221bc8
--- /dev/null
+++
b/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/lal/LALEnvoyAlsTest.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.server.graalvm.lal;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.skywalking.apm.network.logging.v3.LogData;
+import org.apache.skywalking.oap.log.analyzer.provider.LALConfig;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Comparison test for the "envoy-als" rule in lal/envoy-als.yaml.
+ *
+ * <p>This rule uses conditional abort, parsed?.navigation via nested Maps,
+ * tag extraction, and rateLimit sampler with if/else branching.
+ *
+ * <h3>DSL:</h3>
+ * <pre>
+ * filter {
+ * // abort if responseCode < 400 and no responseFlags
+ * if (parsed?.response?.responseCode?.value as Integer < 400
+ * &&
!parsed?.commonProperties?.responseFlags?.toString()?.trim()) {
+ * abort {}
+ * }
+ * extractor {
+ * if (parsed?.response?.responseCode) {
+ * tag 'status.code': parsed?.response?.responseCode?.value
+ * }
+ * tag 'response.flag': parsed?.commonProperties?.responseFlags
+ * }
+ * sink {
+ * sampler {
+ * if (parsed?.commonProperties?.responseFlags?.toString()) {
+ *
rateLimit("${log.service}:${parsed?.commonProperties?.responseFlags?.toString()}")
{ rpm 6000 }
+ * } else {
+ * rateLimit("${log.service}:${parsed?.response?.responseCode}") { rpm
6000 }
+ * }
+ * }
+ * }
+ * }
+ * </pre>
+ *
+ * <h3>Branch coverage:</h3>
+ * <ul>
+ * <li>responseCode < 400 + no flags → abort</li>
+ * <li>responseCode >= 400 + no flags → tags extracted, sampler with
responseCode</li>
+ * <li>responseCode < 400 + flags present → tags extracted, sampler with
flags</li>
+ * </ul>
+ *
+ * <p>Input: Instead of Envoy protobuf extraLog, we set parsed Map directly via
+ * {@code binding.parsed(nestedMap)}. Groovy's {@code map.property} syntax
enables
+ * {@code parsed?.response?.responseCode?.value} to traverse nested Maps.
+ */
+class LALEnvoyAlsTest extends LALScriptComparisonBase {
+
+ private static String ENVOY_ALS_DSL;
+
+ @BeforeAll
+ static void loadRules() throws Exception {
+ final List<LALConfig> rules = loadLALRules("envoy-als.yaml");
+ for (final LALConfig rule : rules) {
+ if ("envoy-als".equals(rule.getName())) {
+ ENVOY_ALS_DSL = rule.getDsl();
+ }
+ }
+ }
+
+ /**
+ * responseCode=200 (< 400), no responseFlags → abort fires.
+ */
+ @Test
+ void lowResponseCodeNoFlags_aborts() {
+ final Map<String, Object> parsedMap = new HashMap<>();
+ parsedMap.put("response", buildMap("responseCode", buildMap("value",
200)));
+ parsedMap.put("commonProperties", buildMap("responseFlags", ""));
+
+ final LogData logData = buildTextLogData(
+ "checkout-svc", "checkout-inst", "", null);
+
+ runAndCompareWithParsedMap(ENVOY_ALS_DSL, logData, parsedMap);
+ }
+
+ /**
+ * responseCode=500 (>= 400), no responseFlags → no abort,
+ * tags extracted ('status.code': 500), sampler uses responseCode.
+ */
+ @Test
+ void highResponseCodeNoFlags_extractsTagsWithResponseCode() {
+ final Map<String, Object> parsedMap = new HashMap<>();
+ parsedMap.put("response", buildMap("responseCode", buildMap("value",
500)));
+ parsedMap.put("commonProperties", buildMap("responseFlags", ""));
+
+ final LogData logData = buildTextLogData(
+ "checkout-svc", "checkout-inst", "", null);
+
+ runAndCompareWithParsedMap(ENVOY_ALS_DSL, logData, parsedMap);
+ }
+
+ /**
+ * responseCode=200 (< 400), responseFlags present → no abort (flags make
+ * the !trim() check fail), tags extracted, sampler uses flags.
+ */
+ @Test
+ void lowResponseCodeWithFlags_extractsTagsWithFlags() {
+ final Map<String, Object> parsedMap = new HashMap<>();
+ parsedMap.put("response", buildMap("responseCode", buildMap("value",
200)));
+ parsedMap.put("commonProperties",
+ buildMap("responseFlags", "{upstreamConnectionFailure}"));
+
+ final LogData logData = buildTextLogData(
+ "checkout-svc", "checkout-inst", "", null);
+
+ runAndCompareWithParsedMap(ENVOY_ALS_DSL, logData, parsedMap);
+ }
+
+ private static Map<String, Object> buildMap(final String key, final Object
value) {
+ final Map<String, Object> map = new HashMap<>();
+ map.put(key, value);
+ return map;
+ }
+}
diff --git
a/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/lal/LALNetworkProfilingTest.java
b/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/lal/LALNetworkProfilingTest.java
new file mode 100644
index 0000000..ac0b04a
--- /dev/null
+++
b/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/lal/LALNetworkProfilingTest.java
@@ -0,0 +1,279 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.server.graalvm.lal;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.skywalking.apm.network.logging.v3.LogData;
+import org.apache.skywalking.oap.log.analyzer.provider.LALConfig;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Comparison test for the "network-profiling-slow-trace" rule shared across
+ * envoy-als.yaml, mesh-dp.yaml, and k8s-service.yaml.
+ *
+ * <p>All three files contain identical DSL, so they compile to the same
+ * pre-compiled class (same SHA-256 hash).
+ *
+ * <h3>DSL (simplified):</h3>
+ * <pre>
+ * filter {
+ * json{}
+ * extractor{
+ * if (tag("LOG_KIND") == "NET_PROFILING_SAMPLED_TRACE") {
+ * sampledTrace {
+ * latency parsed.latency as Long
+ * uri parsed.uri as String
+ * reason parsed.reason as String
+ *
+ * // processId: 3-way if/else-if/else
+ * if (parsed.client_process.process_id as String != "") {
+ * processId parsed.client_process.process_id as String
+ * } else if (parsed.client_process.local as Boolean) {
+ * processId ProcessRegistry.generateVirtualLocalProcess(...)
+ * } else {
+ * processId ProcessRegistry.generateVirtualRemoteProcess(...)
+ * }
+ *
+ * // destProcessId: same 3-way pattern
+ * // detectPoint: direct assignment
+ *
+ * // componentId: 4-way if/else-if chain
+ * if (parsed.component as String == "http" && parsed.ssl as
Boolean) {
+ * componentId 129 // HTTPS
+ * } else if (parsed.component as String == "http") {
+ * componentId 49 // HTTP
+ * } else if (parsed.ssl as Boolean) {
+ * componentId 130 // TLS
+ * } else {
+ * componentId 110 // TCP
+ * }
+ * }
+ * }
+ * }
+ * }
+ * </pre>
+ *
+ * <h3>Branch coverage:</h3>
+ * <ul>
+ * <li>LOG_KIND guard: matching vs non-matching</li>
+ * <li>processId: non-empty process_id (first if-branch)</li>
+ * <li>componentId: all 4 combinations of component + ssl</li>
+ * </ul>
+ *
+ * <p>All tests use non-empty process_id values to avoid ProcessRegistry
+ * static method calls. The else-if/else branches for ProcessRegistry
+ * require static mocking — deferred as lower priority.
+ */
+class LALNetworkProfilingTest extends LALScriptComparisonBase {
+
+ private static String NETWORK_PROFILING_DSL;
+
+ @BeforeAll
+ static void loadRules() throws Exception {
+ // Load from envoy-als.yaml (second rule)
+ final List<LALConfig> rules = loadLALRules("envoy-als.yaml");
+ for (final LALConfig rule : rules) {
+ if ("network-profiling-slow-trace".equals(rule.getName())) {
+ NETWORK_PROFILING_DSL = rule.getDsl();
+ break;
+ }
+ }
+ }
+
+ // ── componentId branch coverage ──
+
+ /**
+ * component="http", ssl=false → componentId=49 (HTTP).
+ */
+ @Test
+ void httpNoSsl_componentId49() {
+ final LogData logData = buildProfilingLogData();
+ final String json = buildProfilingJson("http", false);
+
+ final LogData withJson = buildJsonLogData(
+ logData.getService(), logData.getServiceInstance(), json,
+ profilingTags());
+
+ runAndCompare(NETWORK_PROFILING_DSL, withJson);
+ }
+
+ /**
+ * component="http", ssl=true → componentId=129 (HTTPS).
+ */
+ @Test
+ void httpWithSsl_componentId129() {
+ final LogData logData = buildProfilingLogData();
+ final String json = buildProfilingJson("http", true);
+
+ final LogData withJson = buildJsonLogData(
+ logData.getService(), logData.getServiceInstance(), json,
+ profilingTags());
+
+ runAndCompare(NETWORK_PROFILING_DSL, withJson);
+ }
+
+ /**
+ * component="tcp", ssl=true → componentId=130 (TLS).
+ */
+ @Test
+ void tcpWithSsl_componentId130() {
+ final LogData logData = buildProfilingLogData();
+ final String json = buildProfilingJson("tcp", true);
+
+ final LogData withJson = buildJsonLogData(
+ logData.getService(), logData.getServiceInstance(), json,
+ profilingTags());
+
+ runAndCompare(NETWORK_PROFILING_DSL, withJson);
+ }
+
+ /**
+ * component="tcp", ssl=false → componentId=110 (TCP).
+ */
+ @Test
+ void tcpNoSsl_componentId110() {
+ final LogData logData = buildProfilingLogData();
+ final String json = buildProfilingJson("tcp", false);
+
+ final LogData withJson = buildJsonLogData(
+ logData.getService(), logData.getServiceInstance(), json,
+ profilingTags());
+
+ runAndCompare(NETWORK_PROFILING_DSL, withJson);
+ }
+
+ // ── LOG_KIND guard ──
+
+ /**
+ * LOG_KIND != "NET_PROFILING_SAMPLED_TRACE" → sampledTrace block skipped.
+ */
+ @Test
+ void nonMatchingTag_skipsSampledTrace() {
+ final Map<String, String> tags = new HashMap<>();
+ tags.put("LOG_KIND", "OTHER");
+
+ final LogData logData = buildJsonLogData(
+ "mesh-svc", "mesh-inst", "{}", tags);
+
+ runAndCompare(NETWORK_PROFILING_DSL, logData);
+ }
+
+ // ── Manifest verification ──
+
+ /**
+ * Verify all 3 files resolve to the same pre-compiled class.
+ */
+ /**
+ * Verify all 3 files have structurally identical DSL (may differ in
trailing
+ * whitespace due to YAML parsing), and each resolves to a pre-compiled
class.
+ */
+ @Test
+ void manifestContainsAllThreeRules() throws Exception {
+ final List<LALConfig> envoyRules = loadLALRules("envoy-als.yaml");
+ final List<LALConfig> meshDpRules = loadLALRules("mesh-dp.yaml");
+ final List<LALConfig> k8sRules = loadLALRules("k8s-service.yaml");
+
+ String envoyDsl = null;
+ for (final LALConfig r : envoyRules) {
+ if ("network-profiling-slow-trace".equals(r.getName())) {
+ envoyDsl = r.getDsl();
+ }
+ }
+ final String meshDpDsl = meshDpRules.get(0).getDsl();
+ final String k8sDsl = k8sRules.get(0).getDsl();
+
+ // DSL content is identical (may differ in trailing newline from YAML)
+ assertEquals(envoyDsl.trim(), meshDpDsl.trim(),
+ "envoy-als and mesh-dp should have identical DSL content");
+ assertEquals(envoyDsl.trim(), k8sDsl.trim(),
+ "envoy-als and k8s-service should have identical DSL content");
+
+ // Each DSL hash should resolve in the manifest
+ final Map<String, String> hashManifest = loadManifest();
+ assertTrue(hashManifest.containsKey(sha256(envoyDsl)),
+ "Hash manifest should contain the envoy-als network-profiling DSL
hash");
+ assertTrue(hashManifest.containsKey(sha256(meshDpDsl)),
+ "Hash manifest should contain the mesh-dp network-profiling DSL
hash");
+ assertTrue(hashManifest.containsKey(sha256(k8sDsl)),
+ "Hash manifest should contain the k8s-service network-profiling
DSL hash");
+
+ // Name manifest should contain the rule name
+ final Map<String, String> nameManifest = loadNameManifest();
+ assertTrue(nameManifest.containsKey("network-profiling-slow-trace"),
+ "lal-scripts.txt should contain network-profiling-slow-trace");
+ }
+
+ // ── Helpers ──
+
+ private static LogData buildProfilingLogData() {
+ return LogData.newBuilder()
+ .setService("mesh-svc")
+ .setServiceInstance("mesh-inst")
+ .setTimestamp(System.currentTimeMillis())
+ .build();
+ }
+
+ private static Map<String, String> profilingTags() {
+ final Map<String, String> tags = new HashMap<>();
+ tags.put("LOG_KIND", "NET_PROFILING_SAMPLED_TRACE");
+ return tags;
+ }
+
+ /**
+ * Build JSON body for network profiling with non-empty process_id values
+ * (avoids ProcessRegistry static calls).
+ *
+ * <pre>
+ * {
+ * "latency": 250,
+ * "uri": "/api/checkout",
+ * "reason": "slow",
+ * "client_process": {"process_id": "proc-client-1", "local": false,
"address": "1.2.3.4"},
+ * "server_process": {"process_id": "proc-server-1", "local": false,
"address": "5.6.7.8"},
+ * "detect_point": "server",
+ * "component": "{component}",
+ * "ssl": {ssl},
+ * "service": "mesh-svc",
+ * "serviceInstance": "mesh-inst"
+ * }
+ * </pre>
+ */
+ private static String buildProfilingJson(final String component,
+ final boolean ssl) {
+ return "{"
+ + "\"latency\":250,"
+ + "\"uri\":\"/api/checkout\","
+ + "\"reason\":\"slow\","
+ + "\"client_process\":{\"process_id\":\"proc-client-1\","
+ + "\"local\":false,\"address\":\"1.2.3.4\"},"
+ + "\"server_process\":{\"process_id\":\"proc-server-1\","
+ + "\"local\":false,\"address\":\"5.6.7.8\"},"
+ + "\"detect_point\":\"server\","
+ + "\"component\":\"" + component + "\","
+ + "\"ssl\":" + ssl + ","
+ + "\"service\":\"mesh-svc\","
+ + "\"serviceInstance\":\"mesh-inst\""
+ + "}";
+ }
+}
diff --git
a/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/lal/LALNginxTest.java
b/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/lal/LALNginxTest.java
new file mode 100644
index 0000000..e6c171f
--- /dev/null
+++
b/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/lal/LALNginxTest.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.server.graalvm.lal;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.skywalking.apm.network.logging.v3.LogData;
+import org.apache.skywalking.oap.log.analyzer.provider.LALConfig;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Comparison test for lal/nginx.yaml — two rules exercising text parsing,
+ * conditional tag extraction, timestamp formatting, and metrics generation.
+ *
+ * nginx-access-log: tag("LOG_KIND") guard, text regexp, conditional tag
extraction.
+ * nginx-error-log: tag("LOG_KIND") guard, text regexp, timestamp w/ format,
metrics.
+ */
+class LALNginxTest extends LALScriptComparisonBase {
+
+ private static String ACCESS_LOG_DSL;
+ private static String ERROR_LOG_DSL;
+
+ @BeforeAll
+ static void loadRules() throws Exception {
+ final List<LALConfig> rules = loadLALRules("nginx.yaml");
+ assertEquals(2, rules.size());
+ for (final LALConfig rule : rules) {
+ if ("nginx-access-log".equals(rule.getName())) {
+ ACCESS_LOG_DSL = rule.getDsl();
+ } else if ("nginx-error-log".equals(rule.getName())) {
+ ERROR_LOG_DSL = rule.getDsl();
+ }
+ }
+ }
+
+ // ── nginx-access-log tests ──
+
+ /**
+ * TAG matches NGINX_ACCESS_LOG, text body matches regexp → tag extracted.
+ *
+ * <pre>
+ * 192.168.1.1 - - [15/Jan/2024:10:30:00 +0000] "GET /index.html HTTP/1.1"
200 1234
+ * </pre>
+ */
+ @Test
+ void accessLog_matchingTagAndText_extractsStatusCode() {
+ final Map<String, String> tags = new HashMap<>();
+ tags.put("LOG_KIND", "NGINX_ACCESS_LOG");
+
+ final LogData logData = buildTextLogData(
+ "nginx-svc", "nginx-inst",
+ "192.168.1.1 - - [15/Jan/2024:10:30:00 +0000] "
+ + "\"GET /index.html HTTP/1.1\" 200 1234",
+ tags);
+
+ runAndCompare(ACCESS_LOG_DSL, logData);
+ }
+
+ /**
+ * TAG does not match NGINX_ACCESS_LOG → entire if-block skipped, no
extraction.
+ */
+ @Test
+ void accessLog_nonMatchingTag_noExtraction() {
+ final Map<String, String> tags = new HashMap<>();
+ tags.put("LOG_KIND", "OTHER");
+
+ final LogData logData = buildTextLogData(
+ "nginx-svc", "nginx-inst", "some other log", tags);
+
+ runAndCompare(ACCESS_LOG_DSL, logData);
+ }
+
+ /**
+ * TAG matches but text body doesn't match regexp → parsed.status is null,
+ * no tag added.
+ */
+ @Test
+ void accessLog_matchingTagNonMatchingText_noTag() {
+ final Map<String, String> tags = new HashMap<>();
+ tags.put("LOG_KIND", "NGINX_ACCESS_LOG");
+
+ final LogData logData = buildTextLogData(
+ "nginx-svc", "nginx-inst",
+ "this text does not match the nginx access log format",
+ tags);
+
+ runAndCompare(ACCESS_LOG_DSL, logData);
+ }
+
+ // ── nginx-error-log tests ──
+
+ /**
+ * TAG matches NGINX_ERROR_LOG, text body matches regexp → level tag
extracted,
+ * timestamp parsed with format, metrics generated.
+ *
+ * <pre>
+ * 2024/01/15 10:30:00 [error] 12345#0: upstream error
+ * </pre>
+ */
+ @Test
+ void errorLog_matchingTagAndText_extractsLevelTimestampMetrics() {
+ final Map<String, String> tags = new HashMap<>();
+ tags.put("LOG_KIND", "NGINX_ERROR_LOG");
+
+ final LogData logData = buildTextLogData(
+ "nginx-svc", "nginx-inst",
+ "2024/01/15 10:30:00 [error] 12345#0: upstream error",
+ tags);
+
+ runAndCompare(ERROR_LOG_DSL, logData);
+ }
+
+ /**
+ * TAG does not match NGINX_ERROR_LOG → entire if-block skipped.
+ */
+ @Test
+ void errorLog_nonMatchingTag_noExtraction() {
+ final Map<String, String> tags = new HashMap<>();
+ tags.put("LOG_KIND", "OTHER");
+
+ final LogData logData = buildTextLogData(
+ "nginx-svc", "nginx-inst", "some log", tags);
+
+ runAndCompare(ERROR_LOG_DSL, logData);
+ }
+}
diff --git
a/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/lal/LALScriptComparisonBase.java
b/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/lal/LALScriptComparisonBase.java
new file mode 100644
index 0000000..a211895
--- /dev/null
+++
b/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/lal/LALScriptComparisonBase.java
@@ -0,0 +1,593 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.server.graalvm.lal;
+
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.Message;
+import groovy.lang.GString;
+import groovy.lang.GroovyShell;
+import groovy.transform.CompileStatic;
+import groovy.util.DelegatingScript;
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.Array;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.skywalking.apm.network.common.v3.KeyStringValuePair;
+import org.apache.skywalking.apm.network.logging.v3.LogData;
+import org.apache.skywalking.oap.log.analyzer.dsl.Binding;
+import org.apache.skywalking.oap.log.analyzer.dsl.LALPrecompiledExtension;
+import org.apache.skywalking.oap.log.analyzer.dsl.spec.LALDelegatingScript;
+import org.apache.skywalking.oap.log.analyzer.dsl.spec.filter.FilterSpec;
+import org.apache.skywalking.oap.log.analyzer.module.LogAnalyzerModule;
+import org.apache.skywalking.oap.log.analyzer.provider.LALConfig;
+import org.apache.skywalking.oap.log.analyzer.provider.LALConfigs;
+import org.apache.skywalking.oap.log.analyzer.provider.LogAnalyzerModuleConfig;
+import
org.apache.skywalking.oap.log.analyzer.provider.LogAnalyzerModuleProvider;
+import org.apache.skywalking.oap.meter.analyzer.dsl.SampleFamily;
+import org.apache.skywalking.oap.meter.analyzer.dsl.registry.ProcessRegistry;
+import
org.apache.skywalking.oap.server.analyzer.provider.trace.parser.listener.DatabaseSlowStatementBuilder;
+import
org.apache.skywalking.oap.server.analyzer.provider.trace.parser.listener.SampledTraceBuilder;
+import org.apache.skywalking.oap.server.core.CoreModule;
+import
org.apache.skywalking.oap.server.core.analysis.worker.RecordStreamProcessor;
+import org.apache.skywalking.oap.server.core.config.ConfigService;
+import org.apache.skywalking.oap.server.core.config.NamingControl;
+import org.apache.skywalking.oap.server.core.source.SourceReceiver;
+import org.apache.skywalking.oap.server.library.module.ModuleManager;
+import org.apache.skywalking.oap.server.library.module.ModuleProviderHolder;
+import org.apache.skywalking.oap.server.library.module.ModuleServiceHolder;
+import org.codehaus.groovy.ast.stmt.DoWhileStatement;
+import org.codehaus.groovy.ast.stmt.ForStatement;
+import org.codehaus.groovy.ast.stmt.Statement;
+import org.codehaus.groovy.ast.stmt.WhileStatement;
+import org.codehaus.groovy.control.CompilerConfiguration;
+import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer;
+import org.codehaus.groovy.control.customizers.ImportCustomizer;
+import org.codehaus.groovy.control.customizers.SecureASTCustomizer;
+import org.powermock.reflect.Whitebox;
+
+import static java.util.Collections.singletonList;
+import static java.util.Collections.singletonMap;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Base class for LAL script comparison tests.
+ *
+ * <p>Validates that pre-compiled LAL scripts (built at build time) behave
+ * identically to freshly-compiled scripts. Uses the same dual-path comparison
+ * pattern as {@code MALScriptComparisonBase}.
+ *
+ * <p>Path A: Fresh Groovy compilation with the same CompilerConfiguration
+ * as the build-time LAL DSL.
+ *
+ * <p>Path B: Load pre-compiled class from manifest via SHA-256 hash lookup.
+ */
+abstract class LALScriptComparisonBase {
+ private static volatile Map<String, String> MANIFEST;
+
+ private final ModuleManager mockManager;
+ private final LogAnalyzerModuleConfig config;
+
+ LALScriptComparisonBase() {
+ mockManager = createMockManager();
+ config = new LogAnalyzerModuleConfig();
+ }
+
+ // ── ModuleManager mock setup (from upstream DSLTest) ──
+
+ private static ModuleManager createMockManager() {
+ final ModuleManager manager = mock(ModuleManager.class);
+ Whitebox.setInternalState(manager, "isInPrepareStage", false);
+
when(manager.find(anyString())).thenReturn(mock(ModuleProviderHolder.class));
+
+ // LogAnalyzerModule
+ final LogAnalyzerModuleProvider logProvider =
mock(LogAnalyzerModuleProvider.class);
+
when(logProvider.getMetricConverts()).thenReturn(Collections.emptyList());
+ final ModuleProviderHolder logHolder =
mock(ModuleProviderHolder.class);
+ when(logHolder.provider()).thenReturn(logProvider);
+ when(manager.find(LogAnalyzerModule.NAME)).thenReturn(logHolder);
+
+ // CoreModule
+ final ModuleServiceHolder coreServiceHolder =
mock(ModuleServiceHolder.class);
+ when(coreServiceHolder.getService(SourceReceiver.class))
+ .thenReturn(mock(SourceReceiver.class));
+ final ConfigService configService = mock(ConfigService.class);
+ when(configService.getSearchableLogsTags()).thenReturn("");
+ when(coreServiceHolder.getService(ConfigService.class))
+ .thenReturn(configService);
+ final NamingControl namingControl = mock(NamingControl.class);
+ when(namingControl.formatServiceName(anyString()))
+ .thenAnswer(inv -> inv.getArgument(0));
+ when(coreServiceHolder.getService(NamingControl.class))
+ .thenReturn(namingControl);
+ final ModuleProviderHolder coreHolder =
mock(ModuleProviderHolder.class);
+ when(coreHolder.provider()).thenReturn(coreServiceHolder);
+ when(manager.find(CoreModule.NAME)).thenReturn(coreHolder);
+
+ return manager;
+ }
+
+ // ── Path A: Fresh Groovy compilation ──
+
+ @SuppressWarnings("rawtypes")
+ private DelegatingScript compileGroovy(final String dsl) {
+ final CompilerConfiguration cc = new CompilerConfiguration();
+ final ASTTransformationCustomizer customizer =
+ new ASTTransformationCustomizer(
+ singletonMap("extensions",
+ singletonList(LALPrecompiledExtension.class.getName())),
+ CompileStatic.class);
+ cc.addCompilationCustomizers(customizer);
+
+ final SecureASTCustomizer secureAST = new SecureASTCustomizer();
+ secureAST.setDisallowedStatements(
+ ImmutableList.<Class<? extends Statement>>builder()
+ .add(WhileStatement.class)
+ .add(DoWhileStatement.class)
+ .add(ForStatement.class)
+ .build());
+ secureAST.setAllowedReceiversClasses(
+ ImmutableList.<Class>builder()
+ .add(Object.class)
+ .add(Map.class)
+ .add(List.class)
+ .add(Array.class)
+ .add(GString.class)
+ .add(String.class)
+ .add(ProcessRegistry.class)
+ .build());
+ cc.addCompilationCustomizers(secureAST);
+ cc.setScriptBaseClass(LALDelegatingScript.class.getName());
+
+ final ImportCustomizer icz = new ImportCustomizer();
+ icz.addImport("ProcessRegistry", ProcessRegistry.class.getName());
+ cc.addCompilationCustomizers(icz);
+
+ final GroovyShell sh = new GroovyShell(cc);
+ return (DelegatingScript) sh.parse(dsl, "test_groovy");
+ }
+
+ // ── Path B: Pre-compiled class lookup ──
+
+ private DelegatingScript loadPrecompiled(final String dsl) {
+ final Map<String, String> manifest = loadManifest();
+ final String hash = sha256(dsl);
+ final String className = manifest.get(hash);
+ if (className == null) {
+ throw new AssertionError(
+ "Pre-compiled LAL script not found for hash: " + hash
+ + ". Available: " + manifest.size() + " scripts. "
+ + "Manifest keys: " + manifest.keySet());
+ }
+ try {
+ final Class<?> scriptClass = Class.forName(className);
+ return (DelegatingScript)
scriptClass.getDeclaredConstructor().newInstance();
+ } catch (final Exception e) {
+ throw new AssertionError(
+ "Failed to load pre-compiled LAL class: " + className, e);
+ }
+ }
+
+ // ── Dual-path comparison ──
+
+ /**
+ * Runs both Groovy and pre-compiled paths with the given LogData,
+ * then asserts identical Binding state.
+ */
+ protected void runAndCompare(final String dsl, final LogData logData) {
+ runAndCompareInternal(dsl, logData, null);
+ }
+
+ /**
+ * Variant with extraLog (for envoy-als rules that access parsed?.response
etc).
+ */
+ protected void runAndCompare(final String dsl, final LogData logData,
+ final Message extraLog) {
+ runAndCompareInternal(dsl, logData, extraLog);
+ }
+
+ /**
+ * Variant with pre-populated parsed Map (for envoy-als where parsed comes
+ * from extraLog protobuf normally, but we use Maps for testing).
+ */
+ protected void runAndCompareWithParsedMap(final String dsl,
+ final LogData logData,
+ final Map<String, Object>
parsedMap) {
+ final DelegatingScript scriptA = compileGroovy(dsl);
+ final DelegatingScript scriptB = loadPrecompiled(dsl);
+
+ final EvalResult resultA = evaluateWithParsedMap(scriptA, logData,
parsedMap);
+ final EvalResult resultB = evaluateWithParsedMap(scriptB, logData,
parsedMap);
+
+ assertResultsMatch(resultA, resultB);
+ }
+
+ private void runAndCompareInternal(final String dsl,
+ final LogData logData,
+ final Message extraLog) {
+ final DelegatingScript scriptA = compileGroovy(dsl);
+ final DelegatingScript scriptB = loadPrecompiled(dsl);
+
+ final EvalResult resultA = evaluate(scriptA, logData, extraLog);
+ final EvalResult resultB = evaluate(scriptB, logData, extraLog);
+
+ assertResultsMatch(resultA, resultB);
+ }
+
+ private EvalResult evaluate(final DelegatingScript script,
+ final LogData logData,
+ final Message extraLog) {
+ try {
+ final FilterSpec filterSpec = new FilterSpec(mockManager, config);
+ Whitebox.setInternalState(filterSpec, "sinkListenerFactories",
+ Collections.emptyList());
+ script.setDelegate(filterSpec);
+
+ final Binding binding = new Binding();
+ binding.log(logData.toBuilder().build());
+ if (extraLog != null) {
+ binding.extraLog(extraLog);
+ }
+ final List<SampleFamily> metricsContainer = new ArrayList<>();
+ binding.metricsContainer(metricsContainer);
+ binding.logContainer(new AtomicReference<>());
+
+ // Mock RecordStreamProcessor for sampledTrace
+ mockRecordStreamProcessor();
+
+ filterSpec.bind(binding);
+ script.run();
+
+ return new EvalResult(binding, metricsContainer, null);
+ } catch (final Exception e) {
+ return new EvalResult(null, null, e);
+ }
+ }
+
+ private EvalResult evaluateWithParsedMap(final DelegatingScript script,
+ final LogData logData,
+ final Map<String, Object>
parsedMap) {
+ try {
+ final FilterSpec filterSpec = new FilterSpec(mockManager, config);
+ Whitebox.setInternalState(filterSpec, "sinkListenerFactories",
+ Collections.emptyList());
+ script.setDelegate(filterSpec);
+
+ final Binding binding = new Binding();
+ binding.log(logData.toBuilder().build());
+ binding.parsed(parsedMap);
+ final List<SampleFamily> metricsContainer = new ArrayList<>();
+ binding.metricsContainer(metricsContainer);
+ binding.logContainer(new AtomicReference<>());
+
+ mockRecordStreamProcessor();
+
+ filterSpec.bind(binding);
+ script.run();
+
+ return new EvalResult(binding, metricsContainer, null);
+ } catch (final Exception e) {
+ return new EvalResult(null, null, e);
+ }
+ }
+
+ private static void mockRecordStreamProcessor() {
+ try {
+ final RecordStreamProcessor mockRSP =
mock(RecordStreamProcessor.class);
+ Whitebox.setInternalState(
+ RecordStreamProcessor.class, "PROCESSOR", mockRSP);
+ } catch (final Exception ignored) {
+ // May already be mocked
+ }
+ }
+
+ // ── Assertion helpers ──
+
+ private static void assertResultsMatch(final EvalResult a,
+ final EvalResult b) {
+ if (a.error != null || b.error != null) {
+ // Both should either succeed or fail with same exception type
+ if (a.error != null && b.error != null) {
+ assertEquals(a.error.getClass(), b.error.getClass(),
+ "Exception types differ: A=" + a.error.getMessage()
+ + ", B=" + b.error.getMessage());
+ } else {
+ final String msg = a.error != null
+ ? "Groovy path threw " + a.error + " but pre-compiled
succeeded"
+ : "Pre-compiled path threw " + b.error + " but Groovy
succeeded";
+ throw new AssertionError(msg);
+ }
+ return;
+ }
+
+ // Compare abort/save flags
+ assertEquals(a.binding.shouldAbort(), b.binding.shouldAbort(),
+ "shouldAbort differs");
+ assertEquals(a.binding.shouldSave(), b.binding.shouldSave(),
+ "shouldSave differs");
+
+ if (a.binding.shouldAbort()) {
+ // If aborted, log state may be partially modified. Compare what
we can.
+ return;
+ }
+
+ // Compare LogData.Builder state
+ final LogData.Builder logA = a.binding.log();
+ final LogData.Builder logB = b.binding.log();
+ assertEquals(logA.getService(), logB.getService(), "service differs");
+ assertEquals(logA.getServiceInstance(), logB.getServiceInstance(),
+ "serviceInstance differs");
+ assertEquals(logA.getEndpoint(), logB.getEndpoint(), "endpoint
differs");
+ assertEquals(logA.getLayer(), logB.getLayer(), "layer differs");
+ assertEquals(logA.getTimestamp(), logB.getTimestamp(), "timestamp
differs");
+
+ // Compare tags
+ final List<KeyStringValuePair> tagsA = logA.getTags().getDataList();
+ final List<KeyStringValuePair> tagsB = logB.getTags().getDataList();
+ assertEquals(tagsA.size(), tagsB.size(),
+ "tag count differs: A=" + tagsA + ", B=" + tagsB);
+ for (int i = 0; i < tagsA.size(); i++) {
+ assertEquals(tagsA.get(i).getKey(), tagsB.get(i).getKey(),
+ "tag key at index " + i + " differs");
+ assertEquals(tagsA.get(i).getValue(), tagsB.get(i).getValue(),
+ "tag value at index " + i + " differs");
+ }
+
+ // Compare metrics container
+ assertEquals(a.metrics.size(), b.metrics.size(),
+ "metrics container size differs");
+
+ // Compare databaseSlowStatement if set
+ try {
+ final DatabaseSlowStatementBuilder slowA =
a.binding.databaseSlowStatement();
+ final DatabaseSlowStatementBuilder slowB =
b.binding.databaseSlowStatement();
+ if (slowA != null && slowB != null) {
+ assertEquals(slowA.getId(), slowB.getId(), "slowSql.id
differs");
+ assertEquals(slowA.getStatement(), slowB.getStatement(),
+ "slowSql.statement differs");
+ assertEquals(slowA.getLatency(), slowB.getLatency(),
+ "slowSql.latency differs");
+ } else {
+ assertEquals(slowA, slowB, "slowSql builder presence differs");
+ }
+ } catch (final Exception ignored) {
+ // databaseSlowStatement may not be set
+ }
+
+ // Compare sampledTrace if set
+ try {
+ final SampledTraceBuilder traceA = a.binding.sampledTraceBuilder();
+ final SampledTraceBuilder traceB = b.binding.sampledTraceBuilder();
+ if (traceA != null && traceB != null) {
+ assertEquals(traceA.getUri(), traceB.getUri(),
+ "sampledTrace.uri differs");
+ assertEquals(traceA.getLatency(), traceB.getLatency(),
+ "sampledTrace.latency differs");
+ assertEquals(traceA.getProcessId(), traceB.getProcessId(),
+ "sampledTrace.processId differs");
+ assertEquals(traceA.getDestProcessId(),
traceB.getDestProcessId(),
+ "sampledTrace.destProcessId differs");
+ assertEquals(traceA.getComponentId(), traceB.getComponentId(),
+ "sampledTrace.componentId differs");
+ } else {
+ assertEquals(traceA, traceB,
+ "sampledTrace builder presence differs");
+ }
+ } catch (final Exception ignored) {
+ // sampledTraceBuilder may not be set
+ }
+ }
+
+ // ── YAML loading ──
+
+ /**
+ * Load LAL rules from a YAML file under the lal/ resource directory.
+ */
+ protected static List<LALConfig> loadLALRules(final String yamlFileName)
+ throws Exception {
+ final List<String> fileNames = singletonList(
+ yamlFileName.replace(".yaml", "").replace(".yml", ""));
+ final List<LALConfigs> configs = LALConfigs.load("lal", fileNames);
+ final List<LALConfig> rules = new ArrayList<>();
+ for (final LALConfigs c : configs) {
+ rules.addAll(c.getRules());
+ }
+ return rules;
+ }
+
+ // ── Manifest loading ──
+
+ protected static Map<String, String> loadManifest() {
+ if (MANIFEST != null) {
+ return MANIFEST;
+ }
+ synchronized (LALScriptComparisonBase.class) {
+ if (MANIFEST != null) {
+ return MANIFEST;
+ }
+ final Map<String, String> map = new HashMap<>();
+ try (InputStream is =
LALScriptComparisonBase.class.getClassLoader()
+ .getResourceAsStream("META-INF/lal-scripts-by-hash.txt")) {
+ if (is == null) {
+ throw new AssertionError(
+ "Manifest META-INF/lal-scripts-by-hash.txt not found");
+ }
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(is, StandardCharsets.UTF_8))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ line = line.trim();
+ if (line.isEmpty()) {
+ continue;
+ }
+ final String[] parts = line.split("=", 2);
+ if (parts.length == 2) {
+ map.put(parts[0], parts[1]);
+ }
+ }
+ }
+ } catch (final Exception e) {
+ throw new AssertionError("Failed to load LAL manifest", e);
+ }
+ MANIFEST = map;
+ return map;
+ }
+ }
+
+ /**
+ * Load the script name → class name manifest.
+ */
+ protected static Map<String, String> loadNameManifest() {
+ final Map<String, String> map = new HashMap<>();
+ try (InputStream is = LALScriptComparisonBase.class.getClassLoader()
+ .getResourceAsStream("META-INF/lal-scripts.txt")) {
+ if (is == null) {
+ throw new AssertionError(
+ "Manifest META-INF/lal-scripts.txt not found");
+ }
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(is, StandardCharsets.UTF_8))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ line = line.trim();
+ if (line.isEmpty()) {
+ continue;
+ }
+ final String[] parts = line.split("=", 2);
+ if (parts.length == 2) {
+ map.put(parts[0], parts[1]);
+ }
+ }
+ }
+ } catch (final Exception e) {
+ throw new AssertionError("Failed to load LAL name manifest", e);
+ }
+ return map;
+ }
+
+ // ── LogData builders ──
+
+ /**
+ * Build minimal LogData with text body and optional tags.
+ */
+ protected static LogData buildTextLogData(final String service,
+ final String serviceInstance,
+ final String textBody,
+ final Map<String, String> tags) {
+ final LogData.Builder builder = LogData.newBuilder()
+ .setService(service)
+ .setServiceInstance(serviceInstance)
+ .setTimestamp(System.currentTimeMillis());
+
+ if (textBody != null) {
+ builder.setBody(
+
org.apache.skywalking.apm.network.logging.v3.LogDataBody.newBuilder()
+ .setText(
+
org.apache.skywalking.apm.network.logging.v3.TextLog.newBuilder()
+ .setText(textBody)
+ .build())
+ .build());
+ }
+
+ if (tags != null) {
+ final org.apache.skywalking.apm.network.logging.v3.LogTags.Builder
+ tagsBuilder = org.apache.skywalking.apm.network.logging.v3
+ .LogTags.newBuilder();
+ tags.forEach((k, v) -> tagsBuilder.addData(
+
KeyStringValuePair.newBuilder().setKey(k).setValue(v).build()));
+ builder.setTags(tagsBuilder);
+ }
+
+ return builder.build();
+ }
+
+ /**
+ * Build LogData with JSON body.
+ */
+ protected static LogData buildJsonLogData(final String service,
+ final String serviceInstance,
+ final String jsonBody,
+ final Map<String, String> tags) {
+ final LogData.Builder builder = LogData.newBuilder()
+ .setService(service)
+ .setServiceInstance(serviceInstance)
+ .setTimestamp(System.currentTimeMillis());
+
+ if (jsonBody != null) {
+ builder.setBody(
+
org.apache.skywalking.apm.network.logging.v3.LogDataBody.newBuilder()
+ .setJson(
+
org.apache.skywalking.apm.network.logging.v3.JSONLog.newBuilder()
+ .setJson(jsonBody)
+ .build())
+ .build());
+ }
+
+ if (tags != null) {
+ final org.apache.skywalking.apm.network.logging.v3.LogTags.Builder
+ tagsBuilder = org.apache.skywalking.apm.network.logging.v3
+ .LogTags.newBuilder();
+ tags.forEach((k, v) -> tagsBuilder.addData(
+
KeyStringValuePair.newBuilder().setKey(k).setValue(v).build()));
+ builder.setTags(tagsBuilder);
+ }
+
+ return builder.build();
+ }
+
+ // ── Utility ──
+
+ static String sha256(final String input) {
+ try {
+ final MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ final byte[] hash = digest.digest(
+ input.getBytes(StandardCharsets.UTF_8));
+ final StringBuilder hex = new StringBuilder();
+ for (final byte b : hash) {
+ hex.append(String.format("%02x", b));
+ }
+ return hex.toString();
+ } catch (final Exception e) {
+ throw new RuntimeException("SHA-256 not available", e);
+ }
+ }
+
+ private static final class EvalResult {
+ final Binding binding;
+ final List<SampleFamily> metrics;
+ final Exception error;
+
+ EvalResult(final Binding binding,
+ final List<SampleFamily> metrics,
+ final Exception error) {
+ this.binding = binding;
+ this.metrics = metrics;
+ this.error = error;
+ }
+ }
+}
diff --git
a/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/lal/LALSlowSqlTest.java
b/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/lal/LALSlowSqlTest.java
new file mode 100644
index 0000000..f13079e
--- /dev/null
+++
b/oap-graalvm-server/src/test/java/org/apache/skywalking/oap/server/graalvm/lal/LALSlowSqlTest.java
@@ -0,0 +1,162 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.server.graalvm.lal;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.skywalking.apm.network.logging.v3.LogData;
+import org.apache.skywalking.oap.log.analyzer.provider.LALConfig;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Comparison test for mysql-slowsql.yaml, pgsql-slowsql.yaml, and
redis-slowsql.yaml.
+ *
+ * <p>All three files have identical DSL, so they compile to the same
pre-compiled
+ * class (same SHA-256 hash). Tests run once against the shared DSL and verify
+ * all three rule names exist in the manifest.
+ *
+ * <h3>Shared DSL:</h3>
+ * <pre>
+ * filter {
+ * json{}
+ * extractor{
+ * layer parsed.layer as String
+ * service parsed.service as String
+ * timestamp parsed.time as String
+ * if (tag("LOG_KIND") == "SLOW_SQL") {
+ * slowSql {
+ * id parsed.id as String
+ * statement parsed.statement as String
+ * latency parsed.query_time as Long
+ * }
+ * }
+ * }
+ * }
+ * </pre>
+ *
+ * <h3>Branch coverage:</h3>
+ * <ul>
+ * <li>tag("LOG_KIND") == "SLOW_SQL" → slowSql block executed</li>
+ * <li>tag("LOG_KIND") != "SLOW_SQL" → slowSql block skipped</li>
+ * </ul>
+ */
+class LALSlowSqlTest extends LALScriptComparisonBase {
+
+ private static String SLOW_SQL_DSL;
+
+ @BeforeAll
+ static void loadRules() throws Exception {
+ final List<LALConfig> rules = loadLALRules("mysql-slowsql.yaml");
+ assertEquals(1, rules.size());
+ assertEquals("mysql-slowsql", rules.get(0).getName());
+ SLOW_SQL_DSL = rules.get(0).getDsl();
+ }
+
+ /**
+ * LOG_KIND == "SLOW_SQL" with complete JSON body → json parsed,
+ * layer/service/timestamp extracted, slowSql block executed with
+ * id/statement/latency.
+ *
+ * <pre>
+ * {
+ * "layer": "MYSQL",
+ * "service": "mysql-svc",
+ * "time": "1705305600000",
+ * "id": "stmt-001",
+ * "statement": "SELECT * FROM users WHERE id = 1",
+ * "query_time": 1500
+ * }
+ * </pre>
+ */
+ @Test
+ void slowSqlTag_extractsSlowSqlFields() {
+ final Map<String, String> tags = new HashMap<>();
+ tags.put("LOG_KIND", "SLOW_SQL");
+
+ final LogData logData = buildJsonLogData(
+ "mysql-svc", "mysql-inst",
+ "{\"layer\":\"MYSQL\",\"service\":\"mysql-svc\","
+ + "\"time\":\"1705305600000\","
+ + "\"id\":\"stmt-001\","
+ + "\"statement\":\"SELECT * FROM users WHERE id = 1\","
+ + "\"query_time\":1500}",
+ tags);
+
+ runAndCompare(SLOW_SQL_DSL, logData);
+ }
+
+ /**
+ * LOG_KIND != "SLOW_SQL" → layer/service/timestamp still extracted,
+ * but slowSql block is skipped entirely.
+ */
+ @Test
+ void nonSlowSqlTag_skipsSlowSqlBlock() {
+ final Map<String, String> tags = new HashMap<>();
+ tags.put("LOG_KIND", "NORMAL");
+
+ final LogData logData = buildJsonLogData(
+ "mysql-svc", "mysql-inst",
+ "{\"layer\":\"MYSQL\",\"service\":\"mysql-svc\","
+ + "\"time\":\"1705305600000\","
+ + "\"id\":\"stmt-002\","
+ + "\"statement\":\"INSERT INTO logs\","
+ + "\"query_time\":10}",
+ tags);
+
+ runAndCompare(SLOW_SQL_DSL, logData);
+ }
+
+ /**
+ * Verify all three rule names (mysql, pgsql, redis) exist in the name
manifest
+ * and map to classes that resolve from the same DSL hash.
+ */
+ @Test
+ void manifestContainsAllThreeSlowSqlRules() throws Exception {
+ final Map<String, String> nameManifest = loadNameManifest();
+ assertTrue(nameManifest.containsKey("mysql-slowsql"),
+ "lal-scripts.txt should contain mysql-slowsql");
+ assertTrue(nameManifest.containsKey("pgsql-slowsql"),
+ "lal-scripts.txt should contain pgsql-slowsql");
+ assertTrue(nameManifest.containsKey("redis-slowsql"),
+ "lal-scripts.txt should contain redis-slowsql");
+
+ // Verify DSL from all three files has the same SHA-256
+ final List<LALConfig> mysqlRules = loadLALRules("mysql-slowsql.yaml");
+ final List<LALConfig> pgsqlRules = loadLALRules("pgsql-slowsql.yaml");
+ final List<LALConfig> redisRules = loadLALRules("redis-slowsql.yaml");
+
+ final String mysqlHash = sha256(mysqlRules.get(0).getDsl());
+ final String pgsqlHash = sha256(pgsqlRules.get(0).getDsl());
+ final String redisHash = sha256(redisRules.get(0).getDsl());
+
+ assertEquals(mysqlHash, pgsqlHash,
+ "mysql and pgsql should have identical DSL");
+ assertEquals(mysqlHash, redisHash,
+ "mysql and redis should have identical DSL");
+
+ // All resolve to the same pre-compiled class
+ final Map<String, String> hashManifest = loadManifest();
+ assertTrue(hashManifest.containsKey(mysqlHash),
+ "Hash manifest should contain the shared slow SQL DSL hash");
+ }
+}