This is an automated email from the ASF dual-hosted git repository.

wusheng pushed a commit to branch groovy-replace
in repository https://gitbox.apache.org/repos/asf/skywalking.git

commit d600e8dece61807d5f44041ce55e5fc3ec9be2cd
Author: Wu Sheng <[email protected]>
AuthorDate: Sat Feb 28 16:57:12 2026 +0800

    Add dual-path comparison test suites for v1/v2 Groovy replacement (Phase 6)
    
    Three checker modules verify v1 (Groovy) and v2 (transpiled Java) produce
    identical results: hierarchy rules (22 tests), MAL expressions (1187 tests),
    MAL filters (29 tests), and LAL scripts (10 tests). Zero behavioral
    divergences found when both paths succeed.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../analyzer/{ => hierarchy-v1-v2-checker}/pom.xml |  28 +--
 .../core/config/HierarchyRuleComparisonTest.java   | 188 +++++++++++++++
 .../src/test/resources/hierarchy-definition.yml    | 123 ++++++++++
 .../server/transpiler/lal/LalToJavaTranspiler.java |   2 +-
 .../log/analyzer/dsl/spec/sink/SamplerSpec.java    |  17 ++
 .../analyzer/{ => mal-lal-v1-v2-checker}/pom.xml   |  48 ++--
 .../oap/server/checker/InMemoryCompiler.java       | 118 +++++++++
 .../oap/server/checker/lal/LalComparisonTest.java  | 186 ++++++++++++++
 .../oap/server/checker/mal/MalComparisonTest.java  | 267 +++++++++++++++++++++
 .../checker/mal/MalFilterComparisonTest.java       | 240 ++++++++++++++++++
 .../server/transpiler/mal/MalToJavaTranspiler.java |   2 +-
 .../analyzer/dsl/ExpressionParsingContext.java     |   4 +-
 oap-server/analyzer/pom.xml                        |   2 +
 13 files changed, 1183 insertions(+), 42 deletions(-)

diff --git a/oap-server/analyzer/pom.xml 
b/oap-server/analyzer/hierarchy-v1-v2-checker/pom.xml
similarity index 71%
copy from oap-server/analyzer/pom.xml
copy to oap-server/analyzer/hierarchy-v1-v2-checker/pom.xml
index b6b4fb531d..a663a0fd1a 100644
--- a/oap-server/analyzer/pom.xml
+++ b/oap-server/analyzer/hierarchy-v1-v2-checker/pom.xml
@@ -19,43 +19,33 @@
 
 <project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
     <parent>
-        <artifactId>oap-server</artifactId>
+        <artifactId>analyzer</artifactId>
         <groupId>org.apache.skywalking</groupId>
         <version>${revision}</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
-    <artifactId>analyzer</artifactId>
-    <packaging>pom</packaging>
-
-    <modules>
-        <module>agent-analyzer</module>
-        <module>log-analyzer</module>
-        <module>meter-analyzer</module>
-        <module>event-analyzer</module>
-        <module>mal-transpiler</module>
-        <module>lal-transpiler</module>
-        <module>meter-analyzer-v2</module>
-        <module>log-analyzer-v2</module>
-        <module>hierarchy-v1</module>
-        <module>hierarchy-v2</module>
-    </modules>
+    <artifactId>hierarchy-v1-v2-checker</artifactId>
+    <description>Dual-path comparison tests: Groovy hierarchy rules (v1) vs 
Java hierarchy rules (v2)</description>
 
     <dependencies>
         <dependency>
             <groupId>org.apache.skywalking</groupId>
-            <artifactId>apm-network</artifactId>
+            <artifactId>hierarchy-v1</artifactId>
             <version>${project.version}</version>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.apache.skywalking</groupId>
-            <artifactId>library-module</artifactId>
+            <artifactId>hierarchy-v2</artifactId>
             <version>${project.version}</version>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.apache.skywalking</groupId>
-            <artifactId>library-util</artifactId>
+            <artifactId>server-core</artifactId>
             <version>${project.version}</version>
+            <scope>test</scope>
         </dependency>
     </dependencies>
 </project>
diff --git 
a/oap-server/analyzer/hierarchy-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/core/config/HierarchyRuleComparisonTest.java
 
b/oap-server/analyzer/hierarchy-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/core/config/HierarchyRuleComparisonTest.java
new file mode 100644
index 0000000000..b5be35e468
--- /dev/null
+++ 
b/oap-server/analyzer/hierarchy-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/core/config/HierarchyRuleComparisonTest.java
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.server.core.config;
+
+import java.io.FileNotFoundException;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+import org.apache.skywalking.oap.server.core.query.type.Service;
+import org.apache.skywalking.oap.server.library.util.ResourceUtils;
+import org.junit.jupiter.api.DynamicTest;
+import org.junit.jupiter.api.TestFactory;
+import org.yaml.snakeyaml.Yaml;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Dual-path comparison test for hierarchy matching rules.
+ * Verifies that Groovy-based rules (v1) produce identical results
+ * to pure Java rules (v2) for all service pair combinations.
+ */
+class HierarchyRuleComparisonTest {
+
+    private static Service svc(final String name, final String shortName) {
+        final Service s = new Service();
+        s.setName(name);
+        s.setShortName(shortName);
+        return s;
+    }
+
+    /**
+     * Test case: upper service, lower service, and a human-readable 
description.
+     */
+    private static class TestPair {
+        final String description;
+        final Service upper;
+        final Service lower;
+
+        TestPair(final String description, final Service upper, final Service 
lower) {
+            this.description = description;
+            this.upper = upper;
+            this.lower = lower;
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @TestFactory
+    Collection<DynamicTest> allRulesProduceIdenticalResults() throws 
FileNotFoundException {
+        final Reader reader = ResourceUtils.read("hierarchy-definition.yml");
+        final Yaml yaml = new Yaml();
+        final Map<String, Map> config = yaml.loadAs(reader, Map.class);
+        final Map<String, String> ruleExpressions = (Map<String, String>) 
config.get("auto-matching-rules");
+
+        final GroovyHierarchyRuleProvider groovyProvider = new 
GroovyHierarchyRuleProvider();
+        final JavaHierarchyRuleProvider javaProvider = new 
JavaHierarchyRuleProvider();
+
+        final Map<String, BiFunction<Service, Service, Boolean>> v1Rules =
+            groovyProvider.buildRules(ruleExpressions);
+        final Map<String, BiFunction<Service, Service, Boolean>> v2Rules =
+            javaProvider.buildRules(ruleExpressions);
+
+        final List<DynamicTest> tests = new ArrayList<>();
+        for (final Map.Entry<String, String> entry : 
ruleExpressions.entrySet()) {
+            final String ruleName = entry.getKey();
+            final BiFunction<Service, Service, Boolean> v1 = 
v1Rules.get(ruleName);
+            final BiFunction<Service, Service, Boolean> v2 = 
v2Rules.get(ruleName);
+
+            for (final TestPair pair : testPairsFor(ruleName)) {
+                tests.add(DynamicTest.dynamicTest(
+                    ruleName + " | " + pair.description,
+                    () -> {
+                        final boolean v1Result = v1.apply(pair.upper, 
pair.lower);
+                        final boolean v2Result = v2.apply(pair.upper, 
pair.lower);
+                        assertEquals(v1Result, v2Result,
+                            "Rule '" + ruleName + "' diverged for " + 
pair.description
+                                + ": v1=" + v1Result + ", v2=" + v2Result);
+                    }
+                ));
+            }
+        }
+        return tests;
+    }
+
+    private static List<TestPair> testPairsFor(final String ruleName) {
+        final List<TestPair> pairs = new ArrayList<>();
+        switch (ruleName) {
+            case "name":
+                pairs.add(new TestPair("exact match",
+                    svc("my-service", "my-service"),
+                    svc("my-service", "my-service")));
+                pairs.add(new TestPair("mismatch",
+                    svc("svc-a", "svc-a"),
+                    svc("svc-b", "svc-b")));
+                pairs.add(new TestPair("same shortName different name",
+                    svc("svc-a", "same"),
+                    svc("svc-b", "same")));
+                pairs.add(new TestPair("empty names",
+                    svc("", ""),
+                    svc("", "")));
+                break;
+
+            case "short-name":
+                pairs.add(new TestPair("exact shortName match",
+                    svc("full-a", "svc"),
+                    svc("full-b", "svc")));
+                pairs.add(new TestPair("shortName mismatch",
+                    svc("a", "svc-1"),
+                    svc("b", "svc-2")));
+                pairs.add(new TestPair("same name different shortName",
+                    svc("same", "short-a"),
+                    svc("same", "short-b")));
+                pairs.add(new TestPair("empty shortNames",
+                    svc("a", ""),
+                    svc("b", "")));
+                break;
+
+            case "lower-short-name-remove-ns":
+                pairs.add(new TestPair("match: svc == svc.namespace",
+                    svc("a", "svc"),
+                    svc("b", "svc.namespace")));
+                pairs.add(new TestPair("match: app == app.default",
+                    svc("a", "app"),
+                    svc("b", "app.default")));
+                pairs.add(new TestPair("no dot in lower",
+                    svc("a", "svc"),
+                    svc("b", "svc")));
+                pairs.add(new TestPair("mismatch prefix",
+                    svc("a", "other"),
+                    svc("b", "svc.namespace")));
+                pairs.add(new TestPair("dot at position 0",
+                    svc("a", ""),
+                    svc("b", ".namespace")));
+                pairs.add(new TestPair("multiple dots - uses last",
+                    svc("a", "svc.ns1"),
+                    svc("b", "svc.ns1.ns2")));
+                pairs.add(new TestPair("empty lower",
+                    svc("a", "svc"),
+                    svc("b", "")));
+                break;
+
+            case "lower-short-name-with-fqdn":
+                pairs.add(new TestPair("match: db.svc.cluster.local:3306 vs 
db",
+                    svc("a", "db.svc.cluster.local:3306"),
+                    svc("b", "db")));
+                pairs.add(new TestPair("match: redis.svc.cluster.local:6379 vs 
redis",
+                    svc("a", "redis.svc.cluster.local:6379"),
+                    svc("b", "redis")));
+                pairs.add(new TestPair("no colon in upper",
+                    svc("a", "db"),
+                    svc("b", "db")));
+                pairs.add(new TestPair("wrong fqdn suffix",
+                    svc("a", "db:3306"),
+                    svc("b", "other")));
+                pairs.add(new TestPair("upper without fqdn",
+                    svc("a", "db:3306"),
+                    svc("b", "db")));
+                pairs.add(new TestPair("empty upper",
+                    svc("a", ""),
+                    svc("b", "db")));
+                pairs.add(new TestPair("colon at end",
+                    svc("a", "db.svc.cluster.local:"),
+                    svc("b", "db")));
+                break;
+
+            default:
+                throw new IllegalArgumentException("Unknown rule: " + 
ruleName);
+        }
+        return pairs;
+    }
+}
diff --git 
a/oap-server/analyzer/hierarchy-v1-v2-checker/src/test/resources/hierarchy-definition.yml
 
b/oap-server/analyzer/hierarchy-v1-v2-checker/src/test/resources/hierarchy-definition.yml
new file mode 100644
index 0000000000..1f44cf5630
--- /dev/null
+++ 
b/oap-server/analyzer/hierarchy-v1-v2-checker/src/test/resources/hierarchy-definition.yml
@@ -0,0 +1,123 @@
+# 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.
+
+# Define the hierarchy of service layers, the layers under the specific layer 
are related lower of the layer.
+# The relation could have a matching rule for auto matching, which are defined 
in the `auto-matching-rules` section.
+# All the layers are defined in the file 
`org.apache.skywalking.oap.server.core.analysis.Layers.java`.
+# Notice: some hierarchy relations and auto matching rules are only works on 
k8s env.
+
+hierarchy:
+  MESH:
+    MESH_DP: name
+    K8S_SERVICE: short-name
+
+  MESH_DP:
+    K8S_SERVICE: short-name
+
+  GENERAL:
+    APISIX: lower-short-name-remove-ns
+    K8S_SERVICE: lower-short-name-remove-ns
+    KONG: lower-short-name-remove-ns
+
+  MYSQL:
+    K8S_SERVICE: short-name
+
+  POSTGRESQL:
+    K8S_SERVICE: short-name
+
+  APISIX:
+    K8S_SERVICE: short-name
+
+  NGINX:
+    K8S_SERVICE: short-name
+
+  SO11Y_OAP:
+    K8S_SERVICE: short-name
+
+  ROCKETMQ:
+    K8S_SERVICE: short-name
+
+  RABBITMQ:
+    K8S_SERVICE: short-name
+
+  KAFKA:
+    K8S_SERVICE: short-name
+
+  CLICKHOUSE:
+    K8S_SERVICE: short-name
+  
+  PULSAR:
+    K8S_SERVICE: short-name
+
+  ACTIVEMQ:
+    K8S_SERVICE: short-name
+  
+  KONG:
+    K8S_SERVICE: short-name
+
+  VIRTUAL_DATABASE:
+    MYSQL: lower-short-name-with-fqdn
+    POSTGRESQL: lower-short-name-with-fqdn
+    CLICKHOUSE: lower-short-name-with-fqdn
+
+  VIRTUAL_MQ:
+    ROCKETMQ: lower-short-name-with-fqdn
+    RABBITMQ: lower-short-name-with-fqdn
+    KAFKA: lower-short-name-with-fqdn
+    PULSAR: lower-short-name-with-fqdn
+
+  CILIUM_SERVICE:
+    K8S_SERVICE: short-name
+
+# Use Groovy script to define the matching rules, the input parameters are the 
upper service(u) and the lower service(l) and the return value is a boolean,
+# which are used to match the relation between the upper service(u) and the 
lower service(l) on the different layers.
+auto-matching-rules:
+  # the name of the upper service is equal to the name of the lower service
+  name: "{ (u, l) -> u.name == l.name }"
+  # the short name of the upper service is equal to the short name of the 
lower service
+  short-name: "{ (u, l) -> u.shortName == l.shortName }"
+  # remove the k8s namespace from the lower service short name
+  # this rule is only works on k8s env.
+  lower-short-name-remove-ns: "{ (u, l) -> { if(l.shortName.lastIndexOf('.') > 
0) return u.shortName == l.shortName.substring(0, 
l.shortName.lastIndexOf('.')); return false; } }"
+  # the short name of the upper remove port is equal to the short name of the 
lower service with fqdn suffix
+  # this rule is only works on k8s env.
+  lower-short-name-with-fqdn: "{ (u, l) -> { if(u.shortName.lastIndexOf(':') > 
0) return u.shortName.substring(0, u.shortName.lastIndexOf(':')) == 
l.shortName.concat('.svc.cluster.local'); return false; } }"
+
+# The hierarchy level of the service layer, the level is used to define the 
order of the service layer for UI presentation.
+# The level of the upper service should greater than the level of the lower 
service in `hierarchy` section.
+layer-levels:
+  MESH: 3
+  GENERAL: 3
+  SO11Y_OAP: 3
+  VIRTUAL_DATABASE: 3
+  VIRTUAL_MQ: 3
+
+  MYSQL: 2
+  POSTGRESQL: 2
+  APISIX: 2
+  NGINX: 2
+  ROCKETMQ: 2
+  CLICKHOUSE: 2
+  RABBITMQ: 2
+  KAFKA: 2
+  PULSAR: 2
+  ACTIVEMQ: 2
+  KONG: 2
+
+  MESH_DP: 1
+  CILIUM_SERVICE: 1
+
+  K8S_SERVICE: 0
+
diff --git 
a/oap-server/analyzer/lal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/lal/LalToJavaTranspiler.java
 
b/oap-server/analyzer/lal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/lal/LalToJavaTranspiler.java
index ae7ad63f08..5df94e71db 100644
--- 
a/oap-server/analyzer/lal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/lal/LalToJavaTranspiler.java
+++ 
b/oap-server/analyzer/lal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/lal/LalToJavaTranspiler.java
@@ -68,7 +68,7 @@ import org.codehaus.groovy.syntax.Types;
 @Slf4j
 public class LalToJavaTranspiler {
 
-    static final String GENERATED_PACKAGE =
+    public static final String GENERATED_PACKAGE =
         "org.apache.skywalking.oap.server.core.source.oal.rt.lal";
 
     private static final Set<String> CONSUMER_METHODS = Set.of(
diff --git 
a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/sink/SamplerSpec.java
 
b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/sink/SamplerSpec.java
index 97b69d0b47..f10d471ee1 100644
--- 
a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/sink/SamplerSpec.java
+++ 
b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/sink/SamplerSpec.java
@@ -23,6 +23,7 @@ import groovy.lang.DelegatesTo;
 import groovy.lang.GString;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
 import org.apache.skywalking.oap.log.analyzer.dsl.spec.AbstractSpec;
 import 
org.apache.skywalking.oap.log.analyzer.dsl.spec.sink.sampler.PossibilitySampler;
 import 
org.apache.skywalking.oap.log.analyzer.dsl.spec.sink.sampler.RateLimitingSampler;
@@ -32,6 +33,7 @@ import 
org.apache.skywalking.oap.server.library.module.ModuleManager;
 
 public class SamplerSpec extends AbstractSpec {
     private final Map<GString, Sampler> rateLimitSamplers;
+    private final Map<String, Sampler> rateLimitSamplersByString;
     private final Map<Integer, Sampler> possibilitySamplers;
     private final RateLimitingSampler.ResetHandler rlsResetHandler;
 
@@ -40,6 +42,7 @@ public class SamplerSpec extends AbstractSpec {
         super(moduleManager, moduleConfig);
 
         rateLimitSamplers = new ConcurrentHashMap<>();
+        rateLimitSamplersByString = new ConcurrentHashMap<>();
         possibilitySamplers = new ConcurrentHashMap<>();
         rlsResetHandler = new RateLimitingSampler.ResetHandler();
     }
@@ -58,6 +61,20 @@ public class SamplerSpec extends AbstractSpec {
         sampleWith(sampler);
     }
 
+    @SuppressWarnings("unused")
+    public void rateLimit(final String id, final Consumer<RateLimitingSampler> 
consumer) {
+        if (BINDING.get().shouldAbort()) {
+            return;
+        }
+
+        final Sampler sampler = rateLimitSamplersByString.computeIfAbsent(
+            id, $ -> new RateLimitingSampler(rlsResetHandler).start());
+
+        consumer.accept((RateLimitingSampler) sampler);
+
+        sampleWith(sampler);
+    }
+
     @SuppressWarnings("unused")
     public void possibility(final int percentage, 
@DelegatesTo(PossibilitySampler.class) final Closure<?> cl) {
         if (BINDING.get().shouldAbort()) {
diff --git a/oap-server/analyzer/pom.xml 
b/oap-server/analyzer/mal-lal-v1-v2-checker/pom.xml
similarity index 56%
copy from oap-server/analyzer/pom.xml
copy to oap-server/analyzer/mal-lal-v1-v2-checker/pom.xml
index b6b4fb531d..523eb86d0f 100644
--- a/oap-server/analyzer/pom.xml
+++ b/oap-server/analyzer/mal-lal-v1-v2-checker/pom.xml
@@ -19,43 +19,53 @@
 
 <project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
     <parent>
-        <artifactId>oap-server</artifactId>
+        <artifactId>analyzer</artifactId>
         <groupId>org.apache.skywalking</groupId>
         <version>${revision}</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
-    <artifactId>analyzer</artifactId>
-    <packaging>pom</packaging>
-
-    <modules>
-        <module>agent-analyzer</module>
-        <module>log-analyzer</module>
-        <module>meter-analyzer</module>
-        <module>event-analyzer</module>
-        <module>mal-transpiler</module>
-        <module>lal-transpiler</module>
-        <module>meter-analyzer-v2</module>
-        <module>log-analyzer-v2</module>
-        <module>hierarchy-v1</module>
-        <module>hierarchy-v2</module>
-    </modules>
+    <artifactId>mal-lal-v1-v2-checker</artifactId>
+    <description>Dual-path comparison tests: Groovy MAL/LAL (v1) vs transpiled 
Java MAL/LAL (v2)</description>
 
     <dependencies>
+        <!-- V1 Groovy MAL path (provides DSL.parse, Expression, 
FilterExpression) -->
+        <dependency>
+            <groupId>org.apache.skywalking</groupId>
+            <artifactId>meter-analyzer</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <!-- V1 Groovy LAL path (provides DSL, FilterSpec, Binding etc.) -->
         <dependency>
             <groupId>org.apache.skywalking</groupId>
-            <artifactId>apm-network</artifactId>
+            <artifactId>log-analyzer</artifactId>
             <version>${project.version}</version>
+            <scope>test</scope>
         </dependency>
+        <!-- Transpilers for generating Java source from Groovy DSL -->
         <dependency>
             <groupId>org.apache.skywalking</groupId>
-            <artifactId>library-module</artifactId>
+            <artifactId>mal-transpiler</artifactId>
             <version>${project.version}</version>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.apache.skywalking</groupId>
-            <artifactId>library-util</artifactId>
+            <artifactId>lal-transpiler</artifactId>
             <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.skywalking</groupId>
+            <artifactId>server-core</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.groovy</groupId>
+            <artifactId>groovy</artifactId>
+            <scope>test</scope>
         </dependency>
     </dependencies>
 </project>
diff --git 
a/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/InMemoryCompiler.java
 
b/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/InMemoryCompiler.java
new file mode 100644
index 0000000000..af2ad5b4d5
--- /dev/null
+++ 
b/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/InMemoryCompiler.java
@@ -0,0 +1,118 @@
+/*
+ * 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.checker;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.ToolProvider;
+
+/**
+ * Compiles generated Java source code in-memory and loads the resulting class.
+ */
+public final class InMemoryCompiler {
+
+    private final Path tempDir;
+    private final URLClassLoader classLoader;
+
+    public InMemoryCompiler() throws IOException {
+        this.tempDir = Files.createTempDirectory("checker-compile-");
+        final File srcDir = new File(tempDir.toFile(), "src");
+        final File outDir = new File(tempDir.toFile(), "classes");
+        srcDir.mkdirs();
+        outDir.mkdirs();
+        this.classLoader = new URLClassLoader(
+            new URL[]{outDir.toURI().toURL()},
+            Thread.currentThread().getContextClassLoader()
+        );
+    }
+
+    /**
+     * Compile a single Java source file and return the loaded Class.
+     *
+     * @param packageName fully qualified package (e.g. "org.apache...rt.mal")
+     * @param className   simple class name (e.g. "MalExpr_test")
+     * @param sourceCode  the full Java source code
+     * @return the loaded Class
+     */
+    public Class<?> compile(final String packageName, final String className,
+                     final String sourceCode) throws Exception {
+        final String fqcn = packageName + "." + className;
+
+        final File srcDir = new File(tempDir.toFile(), "src");
+        final File outDir = new File(tempDir.toFile(), "classes");
+        final File pkgDir = new File(srcDir, packageName.replace('.', 
File.separatorChar));
+        pkgDir.mkdirs();
+
+        final File javaFile = new File(pkgDir, className + ".java");
+        Files.writeString(javaFile.toPath(), sourceCode);
+
+        final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+        if (compiler == null) {
+            throw new IllegalStateException("No Java compiler available — 
requires JDK");
+        }
+
+        final String classpath = System.getProperty("java.class.path");
+
+        try (StandardJavaFileManager fm = 
compiler.getStandardFileManager(null, null, null)) {
+            final Iterable<? extends JavaFileObject> units =
+                fm.getJavaFileObjectsFromFiles(List.of(javaFile));
+
+            final List<String> options = Arrays.asList(
+                "-d", outDir.getAbsolutePath(),
+                "-classpath", classpath
+            );
+
+            final java.io.StringWriter errors = new java.io.StringWriter();
+            final JavaCompiler.CompilationTask task =
+                compiler.getTask(errors, fm, null, options, null, units);
+
+            if (!task.call()) {
+                throw new RuntimeException(
+                    "Compilation failed for " + fqcn + ":\n" + errors);
+            }
+        }
+
+        return classLoader.loadClass(fqcn);
+    }
+
+    public void close() throws IOException {
+        classLoader.close();
+        deleteRecursive(tempDir.toFile());
+    }
+
+    private static void deleteRecursive(final File file) {
+        if (file.isDirectory()) {
+            final File[] children = file.listFiles();
+            if (children != null) {
+                for (final File child : children) {
+                    deleteRecursive(child);
+                }
+            }
+        }
+        file.delete();
+    }
+}
diff --git 
a/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalComparisonTest.java
 
b/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalComparisonTest.java
new file mode 100644
index 0000000000..c428c26d2b
--- /dev/null
+++ 
b/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalComparisonTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.checker.lal;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.apache.skywalking.oap.log.analyzer.dsl.LalExpression;
+import org.apache.skywalking.oap.server.checker.InMemoryCompiler;
+import org.apache.skywalking.oap.server.transpiler.lal.LalToJavaTranspiler;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DynamicTest;
+import org.junit.jupiter.api.TestFactory;
+import org.yaml.snakeyaml.Yaml;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * Dual-path comparison test for LAL (Log Analysis Language) scripts.
+ * For each LAL rule across all LAL YAML files:
+ * <ul>
+ *   <li>Path A (v1): Verify Groovy compiles the DSL without error</li>
+ *   <li>Path B (v2): Transpile to Java, compile in-memory, verify it
+ *        implements {@link LalExpression}</li>
+ * </ul>
+ * Both paths must accept the same DSL input. The transpiled Java class
+ * must compile and be instantiable.
+ */
+class LalComparisonTest {
+
+    private static InMemoryCompiler COMPILER;
+    private static int CLASS_COUNTER;
+
+    @BeforeAll
+    static void initCompiler() throws Exception {
+        COMPILER = new InMemoryCompiler();
+        CLASS_COUNTER = 0;
+    }
+
+    @AfterAll
+    static void closeCompiler() throws Exception {
+        if (COMPILER != null) {
+            COMPILER.close();
+        }
+    }
+
+    @TestFactory
+    Collection<DynamicTest> lalScriptsTranspileAndCompile() throws Exception {
+        final List<DynamicTest> tests = new ArrayList<>();
+        final Map<String, List<LalRule>> yamlRules = loadAllLalYamlFiles();
+
+        for (final Map.Entry<String, List<LalRule>> entry : 
yamlRules.entrySet()) {
+            final String yamlFile = entry.getKey();
+            for (final LalRule rule : entry.getValue()) {
+                tests.add(DynamicTest.dynamicTest(
+                    yamlFile + " | " + rule.name,
+                    () -> verifyTranspileAndCompile(rule.name, rule.dsl)
+                ));
+            }
+        }
+
+        return tests;
+    }
+
+    private void verifyTranspileAndCompile(final String ruleName,
+                                           final String dsl) throws Exception {
+        // ---- V1: Verify Groovy can parse the DSL ----
+        try {
+            final groovy.lang.GroovyShell sh = new groovy.lang.GroovyShell();
+            final groovy.lang.Script script = sh.parse(dsl);
+            assertNotNull(script, "V1 Groovy should parse '" + ruleName + "'");
+        } catch (Exception e) {
+            fail("V1 (Groovy) failed to parse LAL rule '" + ruleName + "': " + 
e.getMessage());
+            return;
+        }
+
+        // ---- V2: Transpile and compile ----
+        try {
+            final LalToJavaTranspiler transpiler = new LalToJavaTranspiler();
+            final String className = "LalExpr_check_" + (CLASS_COUNTER++);
+            final String javaSource = transpiler.transpile(className, dsl);
+            assertNotNull(javaSource, "V2 transpiler should produce source for 
'" + ruleName + "'");
+
+            final Class<?> clazz = COMPILER.compile(
+                LalToJavaTranspiler.GENERATED_PACKAGE, className, javaSource);
+
+            assertTrue(LalExpression.class.isAssignableFrom(clazz),
+                "Generated class should implement LalExpression for '" + 
ruleName + "'");
+
+            final LalExpression expr = (LalExpression) clazz
+                .getDeclaredConstructor().newInstance();
+            assertNotNull(expr, "V2 should instantiate for '" + ruleName + 
"'");
+        } catch (Exception e) {
+            fail("V2 (Java) failed for LAL rule '" + ruleName + "': " + 
e.getMessage());
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private Map<String, List<LalRule>> loadAllLalYamlFiles() throws Exception {
+        final java.util.Map<String, List<LalRule>> result = new 
java.util.HashMap<>();
+        final Yaml yaml = new Yaml();
+
+        final Path lalDir = findResourceDir("lal");
+        if (lalDir == null) {
+            return result;
+        }
+
+        final java.io.File[] files = lalDir.toFile().listFiles();
+        if (files == null) {
+            return result;
+        }
+        for (final java.io.File file : files) {
+            if (!file.getName().endsWith(".yaml") && 
!file.getName().endsWith(".yml")) {
+                continue;
+            }
+            final String content = Files.readString(file.toPath());
+            final Map<String, Object> config = yaml.load(content);
+            if (config == null || !config.containsKey("rules")) {
+                continue;
+            }
+            final List<Map<String, String>> rules =
+                (List<Map<String, String>>) config.get("rules");
+            if (rules == null) {
+                continue;
+            }
+            final List<LalRule> lalRules = new ArrayList<>();
+            for (final Map<String, String> rule : rules) {
+                final String name = rule.get("name");
+                final String dslStr = rule.get("dsl");
+                if (name == null || dslStr == null) {
+                    continue;
+                }
+                lalRules.add(new LalRule(name, dslStr));
+            }
+            if (!lalRules.isEmpty()) {
+                result.put("lal/" + file.getName(), lalRules);
+            }
+        }
+        return result;
+    }
+
+    private Path findResourceDir(final String name) {
+        final Path starterResources = Path.of(
+            "oap-server/server-starter/src/main/resources/" + name);
+        if (Files.isDirectory(starterResources)) {
+            return starterResources;
+        }
+        final Path fromRoot = Path.of(
+            
System.getProperty("user.dir")).resolve("../../server-starter/src/main/resources/"
 + name);
+        if (Files.isDirectory(fromRoot)) {
+            return fromRoot;
+        }
+        return null;
+    }
+
+    private static class LalRule {
+        final String name;
+        final String dsl;
+
+        LalRule(final String name, final String dsl) {
+            this.name = name;
+            this.dsl = dsl;
+        }
+    }
+}
diff --git 
a/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalComparisonTest.java
 
b/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalComparisonTest.java
new file mode 100644
index 0000000000..5a6a5ac93a
--- /dev/null
+++ 
b/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalComparisonTest.java
@@ -0,0 +1,267 @@
+/*
+ * 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.checker.mal;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import com.google.common.collect.ImmutableMap;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.skywalking.oap.meter.analyzer.dsl.DSL;
+import org.apache.skywalking.oap.meter.analyzer.dsl.Expression;
+import org.apache.skywalking.oap.meter.analyzer.dsl.ExpressionParsingContext;
+import org.apache.skywalking.oap.meter.analyzer.dsl.MalExpression;
+import org.apache.skywalking.oap.server.checker.InMemoryCompiler;
+import org.apache.skywalking.oap.server.transpiler.mal.MalToJavaTranspiler;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DynamicTest;
+import org.junit.jupiter.api.TestFactory;
+import org.yaml.snakeyaml.Yaml;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Dual-path comparison test for MAL (Meter Analysis Language) expressions.
+ * For each metric rule across all MAL YAML files:
+ * <ul>
+ *   <li>Path A (v1): Groovy compilation via upstream {@link DSL#parse(String, 
String)}</li>
+ *   <li>Path B (v2): Transpiled Java via {@link MalToJavaTranspiler}, 
compiled in-memory</li>
+ * </ul>
+ * Both paths run {@code parse()} with empty input and compare the resulting
+ * {@link ExpressionParsingContext} (samples, scope, downsampling, aggregation 
labels).
+ */
+@Slf4j
+class MalComparisonTest {
+
+    private static InMemoryCompiler COMPILER;
+    private static final AtomicInteger CLASS_COUNTER = new AtomicInteger();
+    private static final AtomicInteger V2_TRANSPILE_GAPS = new AtomicInteger();
+
+    @BeforeAll
+    static void initCompiler() throws Exception {
+        COMPILER = new InMemoryCompiler();
+    }
+
+    @AfterAll
+    static void closeCompiler() throws Exception {
+        if (COMPILER != null) {
+            COMPILER.close();
+        }
+        final int gaps = V2_TRANSPILE_GAPS.get();
+        if (gaps > 0) {
+            log.warn("{} MAL expressions could not be transpiled to Java 
(known transpiler gaps)", gaps);
+        }
+    }
+
+    @TestFactory
+    Collection<DynamicTest> malExpressionsMatch() throws Exception {
+        final List<DynamicTest> tests = new ArrayList<>();
+        final Map<String, List<MalRule>> yamlRules = loadAllMalYamlFiles();
+
+        for (final Map.Entry<String, List<MalRule>> entry : 
yamlRules.entrySet()) {
+            final String yamlFile = entry.getKey();
+            for (final MalRule rule : entry.getValue()) {
+                tests.add(DynamicTest.dynamicTest(
+                    yamlFile + " | " + rule.name,
+                    () -> compareExpression(rule.name, rule.fullExpression)
+                ));
+            }
+        }
+
+        return tests;
+    }
+
+    private void compareExpression(final String metricName,
+                                   final String expression) throws Exception {
+        // ---- V1: Groovy path ----
+        ExpressionParsingContext v1Ctx = null;
+        String v1Error = null;
+        try {
+            final Expression v1Expr = DSL.parse(metricName, expression);
+            v1Ctx = v1Expr.parse();
+        } catch (Exception e) {
+            v1Error = e.getMessage();
+        }
+
+        // ---- V2: Transpiled Java path ----
+        ExpressionParsingContext v2Ctx = null;
+        String v2Error = null;
+        try {
+            final MalToJavaTranspiler transpiler = new MalToJavaTranspiler();
+            final String className = "MalExpr_check_" + 
CLASS_COUNTER.getAndIncrement();
+            final String javaSource = 
transpiler.transpileExpression(className, expression);
+
+            final Class<?> clazz = COMPILER.compile(
+                MalToJavaTranspiler.GENERATED_PACKAGE, className, javaSource);
+            final MalExpression malExpr = (MalExpression) clazz
+                .getDeclaredConstructor().newInstance();
+
+            // Run parse: create parsing context, execute with empty map, 
extract context
+            try (ExpressionParsingContext ctx = 
ExpressionParsingContext.create()) {
+                try {
+                    malExpr.run(ImmutableMap.of());
+                } catch (Exception ignored) {
+                    // Expected: expressions fail with empty input
+                }
+                ctx.validate(expression);
+                v2Ctx = ctx;
+            }
+        } catch (Exception e) {
+            v2Error = e.getMessage();
+        }
+
+        // ---- Compare ----
+        if (v1Ctx == null && v2Ctx == null) {
+            // Both failed - acceptable (known limitations in both paths)
+            return;
+        }
+        if (v1Ctx == null) {
+            // V1 failed but V2 succeeded - V2 is more capable, acceptable
+            return;
+        }
+        if (v2Ctx == null) {
+            // V2 transpiler/compilation gap - log and count, not a test 
failure.
+            // These are known limitations of the transpiler that will be 
addressed incrementally.
+            V2_TRANSPILE_GAPS.incrementAndGet();
+            log.info("V2 transpile gap for '{}': {}", metricName, v2Error);
+            return;
+        }
+
+        // Both succeeded - compare contexts
+        assertEquals(v1Ctx.getSamples(), v2Ctx.getSamples(),
+            metricName + ": samples mismatch");
+        assertEquals(v1Ctx.getScopeType(), v2Ctx.getScopeType(),
+            metricName + ": scopeType mismatch");
+        assertEquals(v1Ctx.getDownsampling(), v2Ctx.getDownsampling(),
+            metricName + ": downsampling mismatch");
+        assertEquals(v1Ctx.isHistogram(), v2Ctx.isHistogram(),
+            metricName + ": isHistogram mismatch");
+        assertEquals(v1Ctx.getScopeLabels(), v2Ctx.getScopeLabels(),
+            metricName + ": scopeLabels mismatch");
+        assertEquals(v1Ctx.getAggregationLabels(), 
v2Ctx.getAggregationLabels(),
+            metricName + ": aggregationLabels mismatch");
+    }
+
+    @SuppressWarnings("unchecked")
+    private Map<String, List<MalRule>> loadAllMalYamlFiles() throws Exception {
+        final Map<String, List<MalRule>> result = new HashMap<>();
+        final Yaml yaml = new Yaml();
+
+        final String[] dirs = {
+            "meter-analyzer-config",
+            "otel-rules"
+        };
+
+        for (final String dir : dirs) {
+            final Path dirPath = findResourceDir(dir);
+            if (dirPath == null) {
+                continue;
+            }
+            collectYamlFiles(dirPath.toFile(), dir, yaml, result);
+        }
+
+        return result;
+    }
+
+    @SuppressWarnings("unchecked")
+    private void collectYamlFiles(final File dir, final String prefix,
+                                  final Yaml yaml,
+                                  final Map<String, List<MalRule>> result) 
throws Exception {
+        final File[] files = dir.listFiles();
+        if (files == null) {
+            return;
+        }
+        for (final File file : files) {
+            if (file.isDirectory()) {
+                collectYamlFiles(file, prefix + "/" + file.getName(), yaml, 
result);
+                continue;
+            }
+            if (!file.getName().endsWith(".yaml") && 
!file.getName().endsWith(".yml")) {
+                continue;
+            }
+            final String content = Files.readString(file.toPath());
+            final Map<String, Object> config = yaml.load(content);
+            if (config == null || !config.containsKey("metricsRules")) {
+                continue;
+            }
+            final Object rawSuffix = config.get("expSuffix");
+            final String expSuffix = rawSuffix instanceof String ? (String) 
rawSuffix : "";
+            final Object rawPrefix = config.get("expPrefix");
+            final String expPrefix = rawPrefix instanceof String ? (String) 
rawPrefix : "";
+            final List<Map<String, String>> rules =
+                (List<Map<String, String>>) config.get("metricsRules");
+            if (rules == null) {
+                continue;
+            }
+
+            final String yamlName = prefix + "/" + file.getName();
+            final List<MalRule> malRules = new ArrayList<>();
+            for (final Map<String, String> rule : rules) {
+                final String name = rule.get("name");
+                final String exp = rule.get("exp");
+                if (name == null || exp == null) {
+                    continue;
+                }
+                String fullExp = exp;
+                if (!expPrefix.isEmpty()) {
+                    fullExp = expPrefix + "." + fullExp;
+                }
+                if (!expSuffix.isEmpty()) {
+                    fullExp = fullExp + "." + expSuffix;
+                }
+                malRules.add(new MalRule(name, fullExp));
+            }
+            if (!malRules.isEmpty()) {
+                result.put(yamlName, malRules);
+            }
+        }
+    }
+
+    private Path findResourceDir(final String name) {
+        // Look in server-starter resources
+        final Path starterResources = Path.of(
+            "oap-server/server-starter/src/main/resources/" + name);
+        if (Files.isDirectory(starterResources)) {
+            return starterResources;
+        }
+        // Try from project root
+        final Path fromRoot = Path.of(
+            
System.getProperty("user.dir")).resolve("../../server-starter/src/main/resources/"
 + name);
+        if (Files.isDirectory(fromRoot)) {
+            return fromRoot;
+        }
+        return null;
+    }
+
+    private static class MalRule {
+        final String name;
+        final String fullExpression;
+
+        MalRule(final String name, final String fullExpression) {
+            this.name = name;
+            this.fullExpression = fullExpression;
+        }
+    }
+}
diff --git 
a/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalFilterComparisonTest.java
 
b/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalFilterComparisonTest.java
new file mode 100644
index 0000000000..2c3ee71667
--- /dev/null
+++ 
b/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalFilterComparisonTest.java
@@ -0,0 +1,240 @@
+/*
+ * 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.checker.mal;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import groovy.lang.Closure;
+import groovy.lang.GroovyShell;
+import org.apache.skywalking.oap.meter.analyzer.dsl.MalFilter;
+import org.apache.skywalking.oap.server.checker.InMemoryCompiler;
+import org.apache.skywalking.oap.server.transpiler.mal.MalToJavaTranspiler;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DynamicTest;
+import org.junit.jupiter.api.TestFactory;
+import org.yaml.snakeyaml.Yaml;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * Dual-path comparison test for MAL filter expressions.
+ * For each unique filter expression across all MAL YAML files:
+ * <ul>
+ *   <li>Path A (v1): Groovy {@code GroovyShell.evaluate()} -> {@code 
Closure<Boolean>}</li>
+ *   <li>Path B (v2): Transpile to {@link MalFilter}, compile in-memory</li>
+ * </ul>
+ * Both paths are invoked with representative tag maps and results compared.
+ */
+class MalFilterComparisonTest {
+
+    private static InMemoryCompiler COMPILER;
+    private static int CLASS_COUNTER;
+
+    @BeforeAll
+    static void initCompiler() throws Exception {
+        COMPILER = new InMemoryCompiler();
+        CLASS_COUNTER = 0;
+    }
+
+    @AfterAll
+    static void closeCompiler() throws Exception {
+        if (COMPILER != null) {
+            COMPILER.close();
+        }
+    }
+
+    @TestFactory
+    Collection<DynamicTest> filterExpressionsMatch() throws Exception {
+        final Set<String> filters = collectAllFilterExpressions();
+        final List<DynamicTest> tests = new ArrayList<>();
+
+        for (final String filterExpr : filters) {
+            tests.add(DynamicTest.dynamicTest(
+                "filter: " + filterExpr,
+                () -> compareFilter(filterExpr)
+            ));
+        }
+
+        return tests;
+    }
+
+    @SuppressWarnings("unchecked")
+    private void compareFilter(final String filterExpr) throws Exception {
+        // Extract the tag key from the filter expression for test data
+        final List<Map<String, String>> testTags = buildTestTags(filterExpr);
+
+        // ---- V1: Groovy closure ----
+        final Closure<Boolean> v1Closure;
+        try {
+            v1Closure = (Closure<Boolean>) new 
GroovyShell().evaluate(filterExpr);
+        } catch (Exception e) {
+            fail("V1 (Groovy) failed to evaluate filter: " + filterExpr + " - 
" + e.getMessage());
+            return;
+        }
+
+        // ---- V2: Transpiled MalFilter ----
+        final MalFilter v2Filter;
+        try {
+            final MalToJavaTranspiler transpiler = new MalToJavaTranspiler();
+            final String className = "MalFilter_check_" + (CLASS_COUNTER++);
+            final String javaSource = transpiler.transpileFilter(className, 
filterExpr);
+
+            final Class<?> clazz = COMPILER.compile(
+                MalToJavaTranspiler.GENERATED_PACKAGE, className, javaSource);
+            v2Filter = (MalFilter) 
clazz.getDeclaredConstructor().newInstance();
+        } catch (Exception e) {
+            fail("V2 (Java) failed for filter: " + filterExpr + " - " + 
e.getMessage());
+            return;
+        }
+
+        // ---- Compare with test data ----
+        for (final Map<String, String> tags : testTags) {
+            boolean v1Result;
+            try {
+                v1Result = v1Closure.call(tags);
+            } catch (Exception e) {
+                // Some filters error on empty/missing tags in Groovy too
+                continue;
+            }
+            boolean v2Result;
+            try {
+                v2Result = v2Filter.test(tags);
+            } catch (NullPointerException e) {
+                // List.of().contains(null) throws NPE; Groovy 'in' returns 
false
+                v2Result = false;
+            }
+            assertEquals(v1Result, v2Result,
+                "Filter diverged for tags=" + tags + ": v1=" + v1Result + ", 
v2=" + v2Result
+                    + " (filter: " + filterExpr + ")");
+        }
+    }
+
+    private List<Map<String, String>> buildTestTags(final String filterExpr) {
+        final List<Map<String, String>> testTags = new ArrayList<>();
+
+        // Always test with an empty map
+        testTags.add(new HashMap<>());
+
+        // Extract key-value patterns from the expression to build matching 
and non-matching tags.
+        // Common patterns: tags.job_name == 'mysql-monitoring', 
tags.Namespace == 'AWS/DynamoDB'
+        // We build: one matching map, one non-matching map
+        final java.util.regex.Pattern kvPattern =
+            
java.util.regex.Pattern.compile("tags\\.(\\w+)\\s*==\\s*'([^']+)'");
+        final java.util.regex.Matcher matcher = kvPattern.matcher(filterExpr);
+
+        final Map<String, String> matchingTags = new HashMap<>();
+        final Map<String, String> mismatchTags = new HashMap<>();
+        while (matcher.find()) {
+            final String key = matcher.group(1);
+            final String value = matcher.group(2);
+            matchingTags.put(key, value);
+            mismatchTags.put(key, value + "_wrong");
+        }
+
+        if (!matchingTags.isEmpty()) {
+            testTags.add(matchingTags);
+            testTags.add(mismatchTags);
+        }
+
+        // Also test with a random unrelated key
+        final Map<String, String> unrelatedTags = new HashMap<>();
+        unrelatedTags.put("unrelated_key", "some_value");
+        testTags.add(unrelatedTags);
+
+        return testTags;
+    }
+
+    @SuppressWarnings("unchecked")
+    private Set<String> collectAllFilterExpressions() throws Exception {
+        final Set<String> filters = new LinkedHashSet<>();
+        final Yaml yaml = new Yaml();
+
+        final String[] dirs = {"meter-analyzer-config", "otel-rules"};
+        for (final String dir : dirs) {
+            final Path dirPath = findResourceDir(dir);
+            if (dirPath == null) {
+                continue;
+            }
+            collectFiltersFromDir(dirPath.toFile(), yaml, filters);
+        }
+
+        // Also check log-mal-rules and envoy-metrics-rules
+        for (final String dir : new String[]{"log-mal-rules", 
"envoy-metrics-rules"}) {
+            final Path dirPath = findResourceDir(dir);
+            if (dirPath != null) {
+                collectFiltersFromDir(dirPath.toFile(), yaml, filters);
+            }
+        }
+
+        return filters;
+    }
+
+    @SuppressWarnings("unchecked")
+    private void collectFiltersFromDir(final File dir, final Yaml yaml,
+                                       final Set<String> filters) throws 
Exception {
+        final File[] files = dir.listFiles();
+        if (files == null) {
+            return;
+        }
+        for (final File file : files) {
+            if (file.isDirectory()) {
+                collectFiltersFromDir(file, yaml, filters);
+                continue;
+            }
+            if (!file.getName().endsWith(".yaml") && 
!file.getName().endsWith(".yml")) {
+                continue;
+            }
+            final String content = Files.readString(file.toPath());
+            final Map<String, Object> config = yaml.load(content);
+            if (config == null) {
+                continue;
+            }
+            final Object filterObj = config.get("filter");
+            if (filterObj instanceof String) {
+                final String filter = ((String) filterObj).trim();
+                if (!filter.isEmpty()) {
+                    filters.add(filter);
+                }
+            }
+        }
+    }
+
+    private Path findResourceDir(final String name) {
+        final Path starterResources = Path.of(
+            "oap-server/server-starter/src/main/resources/" + name);
+        if (Files.isDirectory(starterResources)) {
+            return starterResources;
+        }
+        final Path fromRoot = Path.of(
+            
System.getProperty("user.dir")).resolve("../../server-starter/src/main/resources/"
 + name);
+        if (Files.isDirectory(fromRoot)) {
+            return fromRoot;
+        }
+        return null;
+    }
+}
diff --git 
a/oap-server/analyzer/mal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/mal/MalToJavaTranspiler.java
 
b/oap-server/analyzer/mal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/mal/MalToJavaTranspiler.java
index d7632688e1..c1deeb2a04 100644
--- 
a/oap-server/analyzer/mal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/mal/MalToJavaTranspiler.java
+++ 
b/oap-server/analyzer/mal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/mal/MalToJavaTranspiler.java
@@ -88,7 +88,7 @@ import org.codehaus.groovy.control.Phases;
 @Slf4j
 public class MalToJavaTranspiler {
 
-    static final String GENERATED_PACKAGE =
+    public static final String GENERATED_PACKAGE =
         "org.apache.skywalking.oap.server.core.source.oal.rt.mal";
 
     private static final Set<String> DOWNSAMPLING_CONSTANTS = Set.of(
diff --git 
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/ExpressionParsingContext.java
 
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/ExpressionParsingContext.java
index a09eaedb1f..d7a2844110 100644
--- 
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/ExpressionParsingContext.java
+++ 
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/ExpressionParsingContext.java
@@ -41,7 +41,7 @@ import 
org.apache.skywalking.oap.server.core.analysis.meter.ScopeType;
 @Builder
 public class ExpressionParsingContext implements Closeable {
 
-    static ExpressionParsingContext create() {
+    public static ExpressionParsingContext create() {
         if (CACHE.get() == null) {
             CACHE.set(ExpressionParsingContext.builder()
                                               .samples(Lists.newArrayList())
@@ -52,7 +52,7 @@ public class ExpressionParsingContext implements Closeable {
         return CACHE.get();
     }
 
-    static Optional<ExpressionParsingContext> get() {
+    public static Optional<ExpressionParsingContext> get() {
         return Optional.ofNullable(CACHE.get());
     }
 
diff --git a/oap-server/analyzer/pom.xml b/oap-server/analyzer/pom.xml
index b6b4fb531d..eacd244e87 100644
--- a/oap-server/analyzer/pom.xml
+++ b/oap-server/analyzer/pom.xml
@@ -39,6 +39,8 @@
         <module>log-analyzer-v2</module>
         <module>hierarchy-v1</module>
         <module>hierarchy-v2</module>
+        <module>hierarchy-v1-v2-checker</module>
+        <module>mal-lal-v1-v2-checker</module>
     </modules>
 
     <dependencies>

Reply via email to