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

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


The following commit(s) were added to refs/heads/master by this push:
     new c626093b2a Fix v2 MAL compiler: sanitizeName, filter class generation, 
Groovy truth (#13732)
c626093b2a is described below

commit c626093b2a29e2b9c52eaa3c83db382061022f9b
Author: 吴晟 Wu Sheng <[email protected]>
AuthorDate: Sat Mar 7 11:18:48 2026 +0800

    Fix v2 MAL compiler: sanitizeName, filter class generation, Groovy truth 
(#13732)
    
    1. `sanitizeName` lost digits for numeric-prefixed metric names
    `sanitizeName("4xx")` produced `_xx` (replacing `4` with `_`) instead of 
`_4xx` (prepending `_`). Fixed in `MALCodegenHelper`, `LALCodegenHelper`, and 
`HierarchyRuleClassGenerator` to prepend `_` and keep all valid identifier-part 
characters.
    
    2. Generated class naming: `{yamlFileName}_L{lineNo}_{ruleName}`
    All three generators (`MALClassGenerator`, `LALClassGenerator`, 
`HierarchyRuleClassGenerator`) now use a `buildHintedName()` method that 
combines `yamlSource` (file name + line number) with `classNameHint` (rule 
name) into deterministic class names for stack trace traceability.
    
    Examples:
    - MAL expression: `vm_L25_cpu_total_percentage`
    - MAL filter: `gateway_service_L33_filter`
    - LAL: `execution_basic_L110_if_else_if_warn`
    - Hierarchy: `test_hierarchy_definition_L88_name`
    
    Falls back to `MalExpr_<N>` / `LalExpr_<N>` / `HierarchyRule_<N>` when no 
hint is set.
    
    3. v2 filter `.class` generation in checker tests
    The v1-v2 checker now compiles filter expressions and writes `.class` files 
to `generated-classes/` directories (56 filter classes). Production 
`FilterExpression` accepts `yamlSource` parameter for consistent class naming.
    
    4. SourceFile attributes with YAML traceability
    Generated `.class` files now include the YAML filename and line number in 
the `SourceFile` attribute for both expression and filter classes (e.g., 
`(gateway-service.yaml:33)filter.java`).
    
    5. Groovy truth semantics for filter closures
    v2 filter codegen only checked `!= null` for standalone expressions in 
boolean context (e.g., `tags.ApiId || tags.ApiName`), but Groovy truth also 
requires non-empty string check. Added `MalRuntimeHelper.isTruthy()` runtime 
helper: `null → false`, empty string → `false`, `Boolean.FALSE → false`.
    
    6. LAL and Hierarchy checker enhancements
    - `LalComparisonTest`: added `lineNo` to `LalRule`, set `yamlSource` with 
file name + line number
    - `HierarchyRuleComparisonTest`: set `yamlSource` with file name + line 
number, generates `.class` files
    - LAL test data files renamed from `.input.data` to `.data.yaml` for 
consistency with MAL
    
    7. Documentation updates
    - `dynamic-code-generation-debugging.md`: updated class name examples, 
SourceFile format table, error trace examples
    - `changes.md`: added entries for class naming, filter compilation, 
sanitizeName fix
    - MAL/LAL/Hierarchy `CLAUDE.md`: updated Package & Class Naming sections
---
 .gitignore                                         |  2 +
 docs/en/changes/changes.md                         |  1 +
 .../operation/dynamic-code-generation-debugging.md | 46 +++++------
 oap-server/analyzer/hierarchy/CLAUDE.md            |  8 +-
 .../v2/compiler/HierarchyRuleClassGenerator.java   | 46 +++++++++--
 oap-server/analyzer/log-analyzer/CLAUDE.md         | 10 ++-
 .../analyzer/v2/compiler/LALClassGenerator.java    | 33 +++++++-
 .../log/analyzer/v2/compiler/LALCodegenHelper.java | 14 ++--
 oap-server/analyzer/meter-analyzer/CLAUDE.md       | 11 ++-
 .../skywalking/oap/meter/analyzer/v2/Analyzer.java | 10 ++-
 .../oap/meter/analyzer/v2/MetricConvert.java       | 17 ++++-
 .../analyzer/v2/compiler/MALClassGenerator.java    | 34 ++++++++-
 .../analyzer/v2/compiler/MALClosureCodegen.java    |  5 +-
 .../analyzer/v2/compiler/MALCodegenHelper.java     | 14 ++--
 .../analyzer/v2/compiler/rt/MalRuntimeHelper.java  | 19 +++++
 .../meter/analyzer/v2/dsl/FilterExpression.java    | 21 ++++-
 .../core/config/HierarchyRuleComparisonTest.java   | 19 +++++
 .../mal-lal-v1-v2-checker/CLAUDE.md                |  6 +-
 .../oap/server/checker/lal/LalBenchmark.java       |  4 +-
 .../oap/server/checker/lal/LalComparisonTest.java  | 42 ++++++++--
 .../oap/server/checker/mal/MalComparisonTest.java  | 89 +++++++++++++++++++++-
 ...-basic.input.data => execution-basic.data.yaml} |  0
 .../{default.input.data => default.data.yaml}      |  0
 .../{envoy-als.input.data => envoy-als.data.yaml}  |  0
 ...8s-service.input.data => k8s-service.data.yaml} |  0
 .../{mesh-dp.input.data => mesh-dp.data.yaml}      |  0
 ...-slowsql.input.data => mysql-slowsql.data.yaml} |  0
 ....input.data => network-profiling-e2e.data.yaml} |  0
 .../{nginx.input.data => nginx.data.yaml}          |  0
 ...-slowsql.input.data => pgsql-slowsql.data.yaml} |  0
 ...-slowsql.input.data => redis-slowsql.data.yaml} |  0
 31 files changed, 380 insertions(+), 71 deletions(-)

diff --git a/.gitignore b/.gitignore
index f1e59ed33f..0efd7f68c2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,8 @@ OALLexer.tokens
 .checkstyle
 .externalToolBuilders
 oap-server/oal-grammar/**/gen/
+oap-server/analyzer/log-analyzer/gen/
+oap-server/analyzer/meter-analyzer/gen/
 MQELexer.tokens
 oap-server/mqe-grammar/**/gen/
 PromQLLexer.tokens
diff --git a/docs/en/changes/changes.md b/docs/en/changes/changes.md
index d6e3205e61..3598b3dcb2 100644
--- a/docs/en/changes/changes.md
+++ b/docs/en/changes/changes.md
@@ -15,6 +15,7 @@
   - Explicit context passing replaces Groovy binding/closure capture
   - v1 (Groovy) and v2 (ANTLR4+Javassist) cross-version checker validates 
behavioral equivalence across 1,290+ expressions
   - JMH benchmarks confirm v2 runtime speedups: MAL execute ~6.8x, LAL compile 
~39x / execute ~2.8x, Hierarchy execute ~2.6x faster than Groovy v1
+  - Generated class names follow `{yamlFileName}_L{lineNo}_{ruleName}` pattern 
for all DSLs (MAL/LAL/Hierarchy) for stack trace traceability
 * Fix E2E test metrics verify: make it failure if the metric values all null.
 * Support building, testing, and publishing with Java 25.
 * Add `CLAUDE.md` as AI assistant guide for the project.
diff --git a/docs/en/operation/dynamic-code-generation-debugging.md 
b/docs/en/operation/dynamic-code-generation-debugging.md
index e4a8ff838e..15351fce84 100644
--- a/docs/en/operation/dynamic-code-generation-debugging.md
+++ b/docs/en/operation/dynamic-code-generation-debugging.md
@@ -57,9 +57,9 @@ apache-skywalking-apm-bin/
 │   ├── metrics/              ←   e.g., ServiceRespTimeMetrics.class
 │   ├── metrics/builder/      ←   e.g., ServiceRespTimeMetricsBuilder.class
 │   └── dispatcher/           ←   e.g., ServiceDispatcher.class
-├── mal-rt/                   ← Generated MAL classes (e.g., 
meter_vm_cpu_total_percentage.class)
-├── lal-rt/                   ← Generated LAL classes (e.g., 
default_default.class)
-└── hierarchy-rt/             ← Generated Hierarchy classes (e.g., name.class)
+├── mal-rt/                   ← Generated MAL classes (e.g., 
vm_L25_cpu_total_percentage.class, vm_L20_filter.class)
+├── lal-rt/                   ← Generated LAL classes (e.g., 
default_L3_default.class)
+└── hierarchy-rt/             ← Generated Hierarchy classes (e.g., 
hierarchy_definition_L88_name.class)
 ```
 
 **Docker** (`/skywalking/`):
@@ -120,30 +120,32 @@ Reading this:
 
 ### Format Per DSL
 
-| DSL | SourceFile Example | How to Read |
-|-----|-------------------|-------------|
-| OAL | `(core.oal:20)ServiceRespTimeMetrics.java` | OAL file `core.oal`, line 
20 defines this metric |
-| MAL | `(spring-sleuth.yaml:3)cluster_up_rq_incr.java` | YAML file 
`spring-sleuth.yaml`, rule index 3 (0-based, the 4th `metricsRules` entry) |
-| LAL | `(default.yaml)default_default.java` | YAML file `default.yaml`, rule 
named `default_default` |
-| Hierarchy | `(hierarchy-definition.yml)name.java` | Rule `name` in 
`hierarchy-definition.yml` |
+| DSL | SourceFile Example | Generated Class Name | How to Read |
+|-----|-------------------|---------------------|-------------|
+| OAL | `(core.oal:20)ServiceRespTimeMetrics.java` | `ServiceRespTimeMetrics` 
| OAL file `core.oal`, line 20 defines this metric |
+| MAL | `(vm.yaml:25)cpu_total_percentage.java` | 
`vm_L25_cpu_total_percentage` | YAML file `vm.yaml`, line 25, rule 
`cpu_total_percentage` |
+| MAL filter | `(vm.yaml:20)filter.java` | `vm_L20_filter` | YAML file 
`vm.yaml`, line 20, filter expression |
+| LAL | `(default.yaml:3)default.java` | `default_L3_default` | YAML file 
`default.yaml`, line 3, rule `default` |
+| Hierarchy | `(hierarchy-definition.yml:88)name.java` | 
`hierarchy_definition_L88_name` | Rule `name` at line 88 in 
`hierarchy-definition.yml` |
 
 **Notes:**
-- The number after `:` in the MAL source prefix is the 0-based index of the 
rule within the `metricsRules` list in that YAML file.
-- LAL and Hierarchy rules are standalone entries, so the source prefix 
contains only the YAML file name without a line number.
-- When source information is unavailable, the SourceFile falls back to just 
`ClassName.java` without the parenthesized prefix.
+- The class name pattern is `{yamlFileName}_L{lineNo}_{ruleName}` for all DSLs 
(except OAL).
+  The yaml file name and line number from `yamlSource` are combined with the 
rule name or `filter`.
+- The number after `:` in the SourceFile prefix is the line number in the YAML 
file where the rule is defined (for MAL in production, this may be a 0-based 
rule index instead of a line number).
+- When source information is unavailable, the class name falls back to 
`MalExpr_<N>` / `LalExpr_<N>` / `HierarchyRule_<N>` and the SourceFile to just 
`ClassName.java` without the parenthesized prefix.
 
 ### Mapping Back to DSL Source
 
-1. **Identify the DSL type** from the package or class suffix:
+1. **Identify the DSL type** from the package or class name:
    - `...metrics.generated.*Metrics` or `...Dispatcher` → OAL
-   - `...meter.analyzer.v2.compiler.rt.MalExpr_*` → MAL
-   - `...log.analyzer.v2.compiler.rt.LalExpr_*` → LAL
-   - `...hierarchy.rule.rt.*` → Hierarchy
+   - `...meter.analyzer.v2.compiler.rt.*` → MAL (class name: 
`{yamlName}_L{lineNo}_{ruleName}` or `MalExpr_<N>`)
+   - `...log.analyzer.v2.compiler.rt.*` → LAL (class name: 
`{yamlName}_L{lineNo}_{ruleName}` or `LalExpr_<N>`)
+   - `...hierarchy.rule.rt.*` → Hierarchy (class name: 
`{yamlName}_L{lineNo}_{ruleName}` or `HierarchyRule_<N>`)
 
-2. **Find the source file** from the parenthesized prefix (e.g., `core.oal`, 
`spring-sleuth.yaml`).
-   These files are in the `config/` directory.
+2. **Find the source file** from the parenthesized prefix in the SourceFile 
attribute (e.g., `core.oal`, `vm.yaml`),
+   or from the class name prefix (e.g., `vm_L25_...` → `vm.yaml`). These files 
are in the `config/` directory.
 
-3. **Locate the rule** using the line number (OAL) or rule index (MAL) or rule 
name (LAL/Hierarchy).
+3. **Locate the rule** using the line number from the class name (`_L25_`) or 
the SourceFile prefix (`:25`).
 
 4. **Use the statement number** (after the last `:`) as a rough indicator of 
which operation within the
    generated method failed. Dump the class (see above) and use `javap -v` to 
see the exact mapping.
@@ -168,11 +170,11 @@ MAL and LAL errors during metric processing are caught 
and logged per-expression
 ```
 ERROR o.a.s.o.m.a.v.MetricConvert - Analyze Analyzer{...} error
 java.lang.NullPointerException
-    at ...MalExpr_5.run((vm.yaml:2)meter_vm_cpu_total_percentage.java:5)
+    at 
...vm_L25_cpu_total_percentage.run((vm.yaml:25)cpu_total_percentage.java:5)
 ```
 
-This tells you: the error is in `vm.yaml`, rule index 2 (the 3rd 
`metricsRules` entry),
-metric `meter_vm_cpu_total_percentage`, at statement 5 of the generated 
`run()` method.
+This tells you: the error is in `vm.yaml`, line 25, metric 
`cpu_total_percentage`,
+at statement 5 of the generated `run()` method.
 The processing continues for other metrics — a single expression failure does 
not crash the server.
 
 ### Hierarchy Compilation Failure
diff --git a/oap-server/analyzer/hierarchy/CLAUDE.md 
b/oap-server/analyzer/hierarchy/CLAUDE.md
index 76569131cc..1bd9786944 100644
--- a/oap-server/analyzer/hierarchy/CLAUDE.md
+++ b/oap-server/analyzer/hierarchy/CLAUDE.md
@@ -51,12 +51,14 @@ oap-server/analyzer/hierarchy/
 | Component | Package / Name |
 |-----------|---------------|
 | Parser/Model/Generator | 
`org.apache.skywalking.oap.server.core.config.v2.compiler` |
-| Generated classes | 
`org.apache.skywalking.oap.server.core.config.v2.compiler.hierarchy.rule.rt.HierarchyRule_<N>`
 |
-| Package holder | 
`org.apache.skywalking.oap.server.core.config.v2.compiler.hierarchy.rule.rt.HierarchyRulePackageHolder`
 |
+| Generated classes | `...hierarchy.rule.rt.{yamlName}_L{lineNo}_{ruleName}` |
+| Package holder | `...hierarchy.rule.rt.HierarchyRulePackageHolder` |
 | SPI provider | 
`org.apache.skywalking.oap.server.core.config.v2.compiler.CompiledHierarchyRuleProvider`
 |
 | Service type | `org.apache.skywalking.oap.server.core.query.type.Service` 
(in server-core) |
 
-`<N>` is a global `AtomicInteger` counter.
+Class names are built from `yamlSource` (file name + line number) and 
`classNameHint` (rule name).
+Example: `hierarchy_definition_L88_name` (rule `name` at line 88 of 
`hierarchy-definition.yml`).
+Falls back to `HierarchyRule_<N>` (global counter) when no hint is set.
 
 ## Code Generation Details
 
diff --git 
a/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/v2/compiler/HierarchyRuleClassGenerator.java
 
b/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/v2/compiler/HierarchyRuleClassGenerator.java
index 62e6e37a43..ea9154e47d 100644
--- 
a/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/v2/compiler/HierarchyRuleClassGenerator.java
+++ 
b/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/v2/compiler/HierarchyRuleClassGenerator.java
@@ -104,11 +104,41 @@ public final class HierarchyRuleClassGenerator {
 
     private String makeClassName(final String defaultPrefix) {
         if (classNameHint != null) {
-            return dedupClassName(PACKAGE_PREFIX + 
sanitizeName(classNameHint));
+            return dedupClassName(PACKAGE_PREFIX + buildHintedName());
         }
         return PACKAGE_PREFIX + defaultPrefix + 
CLASS_COUNTER.getAndIncrement();
     }
 
+    /**
+     * Builds class name from {@code yamlSource} + {@code classNameHint}.
+     * Pattern: {@code {yamlBaseName}_L{lineNo}_{hint}} when yamlSource is 
available,
+     * falls back to just {@code {hint}} otherwise.
+     */
+    private String buildHintedName() {
+        final String hint = sanitizeName(classNameHint);
+        if (yamlSource == null) {
+            return hint;
+        }
+        String yamlBase = yamlSource;
+        String lineNo = null;
+        final int colonIdx = yamlSource.lastIndexOf(':');
+        if (colonIdx > 0) {
+            yamlBase = yamlSource.substring(0, colonIdx);
+            lineNo = yamlSource.substring(colonIdx + 1);
+        }
+        final int dotIdx = yamlBase.lastIndexOf('.');
+        if (dotIdx > 0) {
+            yamlBase = yamlBase.substring(0, dotIdx);
+        }
+        final StringBuilder sb = new StringBuilder();
+        sb.append(sanitizeName(yamlBase));
+        if (lineNo != null) {
+            sb.append("_L").append(lineNo);
+        }
+        sb.append('_').append(hint);
+        return sb.toString();
+    }
+
     private String dedupClassName(final String base) {
         if (USED_CLASS_NAMES.add(base)) {
             return base;
@@ -122,14 +152,18 @@ public final class HierarchyRuleClassGenerator {
     }
 
     private static String sanitizeName(final String name) {
-        final StringBuilder sb = new StringBuilder(name.length());
+        if (name == null || name.isEmpty()) {
+            return "Generated";
+        }
+        final StringBuilder sb = new StringBuilder(name.length() + 1);
+        if (!Character.isJavaIdentifierStart(name.charAt(0))) {
+            sb.append('_');
+        }
         for (int i = 0; i < name.length(); i++) {
             final char c = name.charAt(i);
-            sb.append(i == 0
-                ? (Character.isJavaIdentifierStart(c) ? c : '_')
-                : (Character.isJavaIdentifierPart(c) ? c : '_'));
+            sb.append(Character.isJavaIdentifierPart(c) ? c : '_');
         }
-        return sb.length() == 0 ? "Generated" : sb.toString();
+        return sb.toString();
     }
 
     /**
diff --git a/oap-server/analyzer/log-analyzer/CLAUDE.md 
b/oap-server/analyzer/log-analyzer/CLAUDE.md
index 881a40ac7a..ee16ef9e0c 100644
--- a/oap-server/analyzer/log-analyzer/CLAUDE.md
+++ b/oap-server/analyzer/log-analyzer/CLAUDE.md
@@ -61,12 +61,14 @@ All v2 classes live under 
`org.apache.skywalking.oap.log.analyzer.v2.*` to avoid
 | Component | Package / Name |
 |-----------|---------------|
 | Parser/Model/Generator | 
`org.apache.skywalking.oap.log.analyzer.v2.compiler` |
-| Generated classes | 
`org.apache.skywalking.oap.log.analyzer.v2.compiler.rt.LalExpr_<N>` |
+| Generated classes | 
`org.apache.skywalking.oap.log.analyzer.v2.compiler.rt.{yamlName}_L{lineNo}_{ruleName}`
 |
 | Package holder | 
`org.apache.skywalking.oap.log.analyzer.v2.compiler.rt.LalExpressionPackageHolder`
 |
 | Runtime helper | 
`org.apache.skywalking.oap.log.analyzer.v2.compiler.rt.LalRuntimeHelper` |
 | Functional interface | 
`org.apache.skywalking.oap.log.analyzer.v2.dsl.LalExpression` |
 
-`<N>` is a global `AtomicInteger` counter.
+Class names are built from `yamlSource` (file name + line number) and 
`classNameHint` (rule name).
+Example: `default_L3_default` (rule `default` at line 3 of `default.yaml`).
+Falls back to `LalExpr_<N>` (global counter) when no hint is set.
 
 ## Single Class with Private Methods
 
@@ -131,10 +133,10 @@ A single YAML file can have rules with different input 
types (e.g., `envoy-als.y
 
 **Input**: `filter { json {} extractor { service parsed.service as String } 
sink {} }`
 
-One class is generated:
+One class is generated (e.g., `default_L3_my_rule` when 
`yamlSource=default.yaml:3`):
 
 ```java
-public class LalExpr_0 implements LalExpression {
+public class default_L3_my_rule implements LalExpression {
     public void execute(FilterSpec filterSpec, ExecutionContext ctx) {
         LalRuntimeHelper h = new LalRuntimeHelper(ctx);
         filterSpec.json(ctx);
diff --git 
a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGenerator.java
 
b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGenerator.java
index bc1c054cfa..74839f7f12 100644
--- 
a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGenerator.java
+++ 
b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGenerator.java
@@ -181,12 +181,41 @@ public final class LALClassGenerator {
 
     private String makeClassName(final String defaultPrefix) {
         if (classNameHint != null) {
-            return dedupClassName(
-                PACKAGE_PREFIX + LALCodegenHelper.sanitizeName(classNameHint));
+            return dedupClassName(PACKAGE_PREFIX + buildHintedName());
         }
         return PACKAGE_PREFIX + defaultPrefix + 
CLASS_COUNTER.getAndIncrement();
     }
 
+    /**
+     * Builds class name from {@code yamlSource} + {@code classNameHint}.
+     * Pattern: {@code {yamlBaseName}_L{lineNo}_{hint}} when yamlSource is 
available,
+     * falls back to just {@code {hint}} otherwise.
+     */
+    private String buildHintedName() {
+        final String hint = LALCodegenHelper.sanitizeName(classNameHint);
+        if (yamlSource == null) {
+            return hint;
+        }
+        String yamlBase = yamlSource;
+        String lineNo = null;
+        final int colonIdx = yamlSource.lastIndexOf(':');
+        if (colonIdx > 0) {
+            yamlBase = yamlSource.substring(0, colonIdx);
+            lineNo = yamlSource.substring(colonIdx + 1);
+        }
+        final int dotIdx = yamlBase.lastIndexOf('.');
+        if (dotIdx > 0) {
+            yamlBase = yamlBase.substring(0, dotIdx);
+        }
+        final StringBuilder sb = new StringBuilder();
+        sb.append(LALCodegenHelper.sanitizeName(yamlBase));
+        if (lineNo != null) {
+            sb.append("_L").append(lineNo);
+        }
+        sb.append('_').append(hint);
+        return sb.toString();
+    }
+
     private String dedupClassName(final String base) {
         if (USED_CLASS_NAMES.add(base)) {
             return base;
diff --git 
a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALCodegenHelper.java
 
b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALCodegenHelper.java
index 0947ee263c..bb890a65fe 100644
--- 
a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALCodegenHelper.java
+++ 
b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALCodegenHelper.java
@@ -80,14 +80,18 @@ final class LALCodegenHelper {
     }
 
     static String sanitizeName(final String name) {
-        final StringBuilder sb = new StringBuilder(name.length());
+        if (name == null || name.isEmpty()) {
+            return "Generated";
+        }
+        final StringBuilder sb = new StringBuilder(name.length() + 1);
+        if (!Character.isJavaIdentifierStart(name.charAt(0))) {
+            sb.append('_');
+        }
         for (int i = 0; i < name.length(); i++) {
             final char c = name.charAt(i);
-            sb.append(i == 0
-                ? (Character.isJavaIdentifierStart(c) ? c : '_')
-                : (Character.isJavaIdentifierPart(c) ? c : '_'));
+            sb.append(Character.isJavaIdentifierPart(c) ? c : '_');
         }
-        return sb.length() == 0 ? "Generated" : sb.toString();
+        return sb.toString();
     }
 
     static String generateMapValCall(final List<String> keys) {
diff --git a/oap-server/analyzer/meter-analyzer/CLAUDE.md 
b/oap-server/analyzer/meter-analyzer/CLAUDE.md
index 45ec834e4e..f742b99692 100644
--- a/oap-server/analyzer/meter-analyzer/CLAUDE.md
+++ b/oap-server/analyzer/meter-analyzer/CLAUDE.md
@@ -39,7 +39,7 @@ oap-server/analyzer/meter-analyzer/
     MALCodegenHelper.java             — Static utility methods and shared 
constants
     rt/
       MalExpressionPackageHolder.java — Class loading anchor (empty marker)
-      MalRuntimeHelper.java           — Static helpers called by generated 
code (e.g., divReverse)
+      MalRuntimeHelper.java           — Static helpers called by generated 
code (divReverse, regexMatch, isTruthy)
 
   src/test/java/.../compiler/
     MALScriptParserTest.java          — 20 parser tests
@@ -53,12 +53,15 @@ All v2 classes live under 
`org.apache.skywalking.oap.meter.analyzer.v2.*` to avo
 | Component | Package / Name |
 |-----------|---------------|
 | Parser/Model/Generator | 
`org.apache.skywalking.oap.meter.analyzer.v2.compiler` |
-| Generated classes | 
`org.apache.skywalking.oap.meter.analyzer.v2.compiler.rt.MalExpr_<N>` |
+| Generated classes | 
`org.apache.skywalking.oap.meter.analyzer.v2.compiler.rt.{yamlName}_L{lineNo}_{ruleName}`
 |
+| Filter classes | 
`org.apache.skywalking.oap.meter.analyzer.v2.compiler.rt.{yamlName}_L{lineNo}_filter`
 |
 | Package holder | 
`org.apache.skywalking.oap.meter.analyzer.v2.compiler.rt.MalExpressionPackageHolder`
 |
 | Runtime helper | 
`org.apache.skywalking.oap.meter.analyzer.v2.compiler.rt.MalRuntimeHelper` |
 | Functional interface | 
`org.apache.skywalking.oap.meter.analyzer.v2.dsl.MalExpression` |
 
-`<N>` is a global `AtomicInteger` counter.
+Class names are built from `yamlSource` (file name + line number) and 
`classNameHint` (rule name or `filter`).
+Example: `vm_L25_cpu_total_percentage` (expression), 
`gateway_service_L33_filter` (filter).
+Falls back to `MalExpr_<N>` (global counter) when no hint is set.
 
 ## Javassist Constraints
 
@@ -96,7 +99,7 @@ public ExpressionMetadata metadata() {
 
 **Input with closure**: `metric.tag({ tags -> tags['k'] = 'v' })`
 
-One class is generated (`MalExpr_0`):
+One class is generated (e.g., `vm_L5_my_metric` when `yamlSource=vm.yaml:5`):
 - Method `_tag_apply(Map tags)` — contains `tags.put("k", "v"); return tags;`
 - Field `_tag` — typed as `TagFunction`, wired via `LambdaMetafactory` after 
class loading
 - `run()` body calls `metric.tag(this._tag)`
diff --git 
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/Analyzer.java
 
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/Analyzer.java
index d07fcfde40..fa65613cbe 100644
--- 
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/Analyzer.java
+++ 
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/Analyzer.java
@@ -115,11 +115,19 @@ public class Analyzer {
                                  final String expression,
                                  final MeterSystem meterSystem,
                                  final String yamlSource) {
-        Expression e = DSL.parse(metricName, expression, yamlSource);
         FilterExpression filter = null;
         if (!Strings.isNullOrEmpty(filterExpression)) {
             filter = new FilterExpression(filterExpression);
         }
+        return build(metricName, filter, expression, meterSystem, yamlSource);
+    }
+
+    public static Analyzer build(final String metricName,
+                                 final FilterExpression filter,
+                                 final String expression,
+                                 final MeterSystem meterSystem,
+                                 final String yamlSource) {
+        Expression e = DSL.parse(metricName, expression, yamlSource);
         ExpressionMetadata ctx = e.parse();
         Analyzer analyzer = new Analyzer(metricName, filter, e, meterSystem, 
ctx);
         analyzer.init();
diff --git 
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/MetricConvert.java
 
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/MetricConvert.java
index 2891937864..34064d5486 100644
--- 
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/MetricConvert.java
+++ 
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/MetricConvert.java
@@ -28,6 +28,7 @@ import java.util.stream.IntStream;
 import java.util.stream.Stream;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.skywalking.oap.meter.analyzer.v2.dsl.FilterExpression;
 import org.apache.skywalking.oap.meter.analyzer.v2.dsl.SampleFamily;
 import org.apache.skywalking.oap.server.core.analysis.meter.MeterSystem;
 
@@ -75,6 +76,7 @@ public class MetricConvert {
     public MetricConvert(MetricRuleConfig rule, MeterSystem service) {
         
Preconditions.checkState(!Strings.isNullOrEmpty(rule.getMetricPrefix()));
         final String sourceName = rule.getSourceName();
+        final FilterExpression filter = buildFilter(rule);
         final List<? extends MetricRuleConfig.RuleConfig> rules = 
rule.getMetricsRules();
         this.analyzers = IntStream.range(0, rules.size()).mapToObj(
             i -> {
@@ -83,7 +85,7 @@ public class MetricConvert {
                     ? sourceName + ".yaml:" + i : null;
                 return buildAnalyzer(
                     formatMetricName(rule, r.getName()),
-                    rule.getFilter(),
+                    filter,
                     formatExp(rule.getExpPrefix(), rule.getExpSuffix(), 
r.getExp()),
                     service,
                     yamlSource
@@ -93,7 +95,7 @@ public class MetricConvert {
     }
 
     Analyzer buildAnalyzer(final String metricsName,
-                           final String filter,
+                           final FilterExpression filter,
                            final String exp,
                            final MeterSystem service,
                            final String yamlSource) {
@@ -106,6 +108,17 @@ public class MetricConvert {
         );
     }
 
+    private static FilterExpression buildFilter(final MetricRuleConfig rule) {
+        final String filterText = rule.getFilter();
+        if (Strings.isNullOrEmpty(filterText)) {
+            return null;
+        }
+        final String sourceName = rule.getSourceName();
+        final String yamlSource = sourceName != null
+            ? sourceName + ".yaml" : null;
+        return new FilterExpression(filterText, "filter", yamlSource);
+    }
+
     private String formatExp(final String expPrefix, String expSuffix, String 
exp) {
         String ret = exp;
         if (!Strings.isNullOrEmpty(expPrefix)) {
diff --git 
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGenerator.java
 
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGenerator.java
index e55a50bfe5..cfaff7d829 100644
--- 
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGenerator.java
+++ 
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGenerator.java
@@ -103,11 +103,41 @@ public final class MALClassGenerator {
 
     private String makeClassName(final String defaultPrefix) {
         if (classNameHint != null) {
-            return dedupClassName(PACKAGE_PREFIX + 
MALCodegenHelper.sanitizeName(classNameHint));
+            return dedupClassName(PACKAGE_PREFIX + buildHintedName());
         }
         return PACKAGE_PREFIX + defaultPrefix + 
CLASS_COUNTER.getAndIncrement();
     }
 
+    /**
+     * Builds class name from {@code yamlSource} + {@code classNameHint}.
+     * Pattern: {@code {yamlBaseName}_L{lineNo}_{hint}} when yamlSource is 
available,
+     * falls back to just {@code {hint}} otherwise.
+     */
+    private String buildHintedName() {
+        final String hint = MALCodegenHelper.sanitizeName(classNameHint);
+        if (yamlSource == null) {
+            return hint;
+        }
+        String yamlBase = yamlSource;
+        String lineNo = null;
+        final int colonIdx = yamlSource.lastIndexOf(':');
+        if (colonIdx > 0) {
+            yamlBase = yamlSource.substring(0, colonIdx);
+            lineNo = yamlSource.substring(colonIdx + 1);
+        }
+        final int dotIdx = yamlBase.lastIndexOf('.');
+        if (dotIdx > 0) {
+            yamlBase = yamlBase.substring(0, dotIdx);
+        }
+        final StringBuilder sb = new StringBuilder();
+        sb.append(MALCodegenHelper.sanitizeName(yamlBase));
+        if (lineNo != null) {
+            sb.append("_L").append(lineNo);
+        }
+        sb.append('_').append(hint);
+        return sb.toString();
+    }
+
     private String dedupClassName(final String base) {
         if (USED_CLASS_NAMES.add(base)) {
             return base;
@@ -372,6 +402,8 @@ public final class MALClassGenerator {
             {paramName, "Ljava/util/Map;"}
         });
         addLineNumberTable(testMethod, 2); // slot 0=this, 1=samples
+        setSourceFile(ctClass, formatSourceFileName(
+            classNameHint != null ? classNameHint : "filter"));
 
         writeClassFile(ctClass);
 
diff --git 
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClosureCodegen.java
 
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClosureCodegen.java
index 4db10cf2b9..254289b4f5 100644
--- 
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClosureCodegen.java
+++ 
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClosureCodegen.java
@@ -761,9 +761,10 @@ final class MALClosureCodegen {
             if (MALCodegenHelper.isBooleanExpression(condExpr)) {
                 generateClosureExpr(sb, condExpr, paramName, beanMode);
             } else {
-                sb.append("(");
+                // Groovy truth: non-null, non-empty string, non-false
+                
sb.append(MALCodegenHelper.RUNTIME_HELPER_FQCN).append(".isTruthy(");
                 generateClosureExpr(sb, condExpr, paramName, beanMode);
-                sb.append(" != null)");
+                sb.append(")");
             }
         } else if (cond instanceof MALExpressionModel.ClosureInCondition) {
             final MALExpressionModel.ClosureInCondition ic =
diff --git 
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALCodegenHelper.java
 
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALCodegenHelper.java
index 072a7e2b4a..8f02be1df5 100644
--- 
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALCodegenHelper.java
+++ 
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALCodegenHelper.java
@@ -102,14 +102,18 @@ final class MALCodegenHelper {
     // ---- Static utility methods ----
 
     static String sanitizeName(final String name) {
-        final StringBuilder sb = new StringBuilder(name.length());
+        if (name == null || name.isEmpty()) {
+            return "Generated";
+        }
+        final StringBuilder sb = new StringBuilder(name.length() + 1);
+        if (!Character.isJavaIdentifierStart(name.charAt(0))) {
+            sb.append('_');
+        }
         for (int i = 0; i < name.length(); i++) {
             final char c = name.charAt(i);
-            sb.append(i == 0
-                ? (Character.isJavaIdentifierStart(c) ? c : '_')
-                : (Character.isJavaIdentifierPart(c) ? c : '_'));
+            sb.append(Character.isJavaIdentifierPart(c) ? c : '_');
         }
-        return sb.length() == 0 ? "Generated" : sb.toString();
+        return sb.toString();
     }
 
     static String escapeJava(final String s) {
diff --git 
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/rt/MalRuntimeHelper.java
 
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/rt/MalRuntimeHelper.java
index 1e8dfc0d5f..669be6ef7c 100644
--- 
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/rt/MalRuntimeHelper.java
+++ 
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/rt/MalRuntimeHelper.java
@@ -59,6 +59,25 @@ public final class MalRuntimeHelper {
      * Reverse division: computes {@code numerator / v} for each sample value 
{@code v}.
      * Used by generated code for {@code Number / SampleFamily} expressions.
      */
+    /**
+     * Groovy truth check: {@code null → false}, empty string → {@code false},
+     * {@code Boolean.FALSE → false}, everything else → {@code true}.
+     * Used by generated filter code for standalone expressions in boolean 
context
+     * (e.g., {@code tags.ApiId || tags.ApiName}).
+     */
+    public static boolean isTruthy(final Object value) {
+        if (value == null) {
+            return false;
+        }
+        if (value instanceof Boolean) {
+            return (Boolean) value;
+        }
+        if (value instanceof CharSequence) {
+            return ((CharSequence) value).length() > 0;
+        }
+        return true;
+    }
+
     public static SampleFamily divReverse(final double numerator,
                                           final SampleFamily sf) {
         if (sf == SampleFamily.EMPTY) {
diff --git 
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/FilterExpression.java
 
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/FilterExpression.java
index f5fd74696a..8464f8617d 100644
--- 
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/FilterExpression.java
+++ 
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/FilterExpression.java
@@ -37,9 +37,28 @@ public class FilterExpression {
     private final MalFilter malFilter;
 
     public FilterExpression(final String literal) {
+        this(literal, null, null);
+    }
+
+    public FilterExpression(final String literal, final String filterNameHint) 
{
+        this(literal, filterNameHint, null);
+    }
+
+    public FilterExpression(final String literal,
+                            final String filterNameHint,
+                            final String yamlSource) {
         this.literal = literal;
         try {
-            this.malFilter = GENERATOR.compileFilter(literal);
+            if (filterNameHint != null) {
+                GENERATOR.setClassNameHint(filterNameHint);
+            }
+            GENERATOR.setYamlSource(yamlSource);
+            try {
+                this.malFilter = GENERATOR.compileFilter(literal);
+            } finally {
+                GENERATOR.setClassNameHint(null);
+                GENERATOR.setYamlSource(null);
+            }
         } catch (Exception e) {
             throw new IllegalStateException(
                 "Failed to compile MAL filter expression: " + literal, e);
diff --git 
a/test/script-cases/script-runtime-with-groovy/hierarchy-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/core/config/HierarchyRuleComparisonTest.java
 
b/test/script-cases/script-runtime-with-groovy/hierarchy-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/core/config/HierarchyRuleComparisonTest.java
index 1f6a7a3e3a..392e9ef205 100644
--- 
a/test/script-cases/script-runtime-with-groovy/hierarchy-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/core/config/HierarchyRuleComparisonTest.java
+++ 
b/test/script-cases/script-runtime-with-groovy/hierarchy-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/core/config/HierarchyRuleComparisonTest.java
@@ -90,13 +90,19 @@ class HierarchyRuleComparisonTest {
             .replaceFirst("\\.(yaml|yml)$", "");
         final File classBaseDir = new File(hierarchyYml.getParent().toFile(),
             baseName + ".generated-classes");
+        final String yamlContent = Files.readString(hierarchyYml);
+        final String[] yamlLines = yamlContent.split("\n");
         final HierarchyRuleClassGenerator generator = new 
HierarchyRuleClassGenerator();
         generator.setClassOutputDir(classBaseDir);
         final java.util.Map<String, BiFunction<Service, Service, Boolean>> 
v2Rules =
             new java.util.HashMap<>();
         for (final Map.Entry<String, String> entry : 
ruleExpressions.entrySet()) {
             final String ruleName = entry.getKey();
+            final int lineNo = findRuleLine(yamlLines, ruleName);
             generator.setClassNameHint(ruleName);
+            generator.setYamlSource(lineNo > 0
+                ? hierarchyYml.getFileName().toString() + ":" + lineNo
+                : hierarchyYml.getFileName().toString());
             v2Rules.put(ruleName, generator.compile(ruleName, 
entry.getValue()));
         }
 
@@ -174,6 +180,19 @@ class HierarchyRuleComparisonTest {
         return result;
     }
 
+    /**
+     * Find the 1-based line number of {@code ruleName:} in the YAML.
+     */
+    private static int findRuleLine(final String[] lines, final String 
ruleName) {
+        for (int i = 0; i < lines.length; i++) {
+            final String trimmed = lines[i].trim();
+            if (trimmed.startsWith(ruleName + ":")) {
+                return i + 1;
+            }
+        }
+        return 0;
+    }
+
     private Path findHierarchyDefinition() {
         final String[] candidates = {
             
"test/script-cases/scripts/hierarchy-rule/test-hierarchy-definition.yml",
diff --git 
a/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/CLAUDE.md 
b/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/CLAUDE.md
index bced8ed592..f986ffbf56 100644
--- 
a/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/CLAUDE.md
+++ 
b/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/CLAUDE.md
@@ -19,7 +19,7 @@ For each DSL expression:
 2. Compare compile-time metadata (sample names, scope type, aggregation 
labels, etc.)
 3. Execute both with identical mock input data
 4. Assert output samples match (entities, labels, values)
-5. Validate against expected data in `.data.yaml` / `.input.data`
+5. Validate against expected data in `.data.yaml`
 
 ## Script Directories (MAL)
 
@@ -56,9 +56,9 @@ Each MAL rule YAML has a companion `.data.yaml` with `input` 
and `expected` sect
 - Standard rules use `metricsRules` key
 - Zabbix rules use `metrics` key (both are handled by the collector)
 
-### LAL (.input.data files)
+### LAL (.data.yaml files)
 
-Each LAL rule YAML has a companion `.input.data` with per-rule test entries.
+Each LAL rule YAML has a companion `.data.yaml` with per-rule test entries.
 
 **Entry structure:** service, body-type, body, optional tags/extra-log, expect 
assertions.
 
diff --git 
a/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalBenchmark.java
 
b/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalBenchmark.java
index 30bc52e208..5b2d7b9bce 100644
--- 
a/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalBenchmark.java
+++ 
b/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalBenchmark.java
@@ -126,8 +126,8 @@ public class LalBenchmark {
         final List<Map<String, String>> ruleConfigs =
             (List<Map<String, String>>) config.get("rules");
 
-        // Load envoy-als.input.data
-        final Path inputDataPath = 
lalYaml.getParent().resolve("envoy-als.input.data");
+        // Load envoy-als.data.yaml
+        final Path inputDataPath = 
lalYaml.getParent().resolve("envoy-als.data.yaml");
         Map<String, Map<String, Object>> inputData = null;
         if (Files.isRegularFile(inputDataPath)) {
             inputData = yaml.load(Files.readString(inputDataPath));
diff --git 
a/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalComparisonTest.java
 
b/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalComparisonTest.java
index 3efde00b39..51e9c4aa04 100644
--- 
a/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalComparisonTest.java
+++ 
b/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalComparisonTest.java
@@ -139,7 +139,10 @@ class LalComparisonTest {
             generator.setClassOutputDir(new java.io.File(
                 rule.sourceFile.getParent(),
                 baseName + ".generated-classes"));
-            generator.setClassNameHint(baseName + "_" + rule.name);
+            generator.setClassNameHint(rule.name);
+            generator.setYamlSource(rule.lineNo > 0
+                ? rule.sourceFile.getName() + ":" + rule.lineNo
+                : rule.sourceFile.getName());
         }
         return generator.compile(rule.dsl);
     }
@@ -648,11 +651,11 @@ class LalComparisonTest {
                 continue;
             }
 
-            // Load matching .input.data file if present
+            // Load matching .data.yaml file if present
             final String baseName = file.getName()
                 .replaceFirst("\\.(yaml|yml)$", "");
             final File inputDataFile = new File(file.getParent(),
-                baseName + ".input.data");
+                baseName + ".data.yaml");
             Map<String, Map<String, Object>> inputData = null;
             if (inputDataFile.exists()) {
                 inputData = yaml.load(
@@ -660,6 +663,8 @@ class LalComparisonTest {
             }
 
             final List<LalRule> lalRules = new ArrayList<>();
+            final String[] lines = content.split("\n");
+            final Map<String, Integer> nameCount = new HashMap<>();
             for (final Map<String, String> rule : rules) {
                 final String name = rule.get("name");
                 final String dslStr = rule.get("dsl");
@@ -668,6 +673,8 @@ class LalComparisonTest {
                 }
                 final String extraLogType = rule.get("extraLogType");
                 final String layer = rule.get("layer");
+                final int count = nameCount.merge(name, 1, Integer::sum);
+                final int lineNo = findRuleLine(lines, name, count);
 
                 final Object ruleInput = inputData != null
                     ? inputData.get(name) : null;
@@ -682,7 +689,7 @@ class LalComparisonTest {
                     inputs = Collections.emptyList();
                 }
                 lalRules.add(new LalRule(
-                    name, dslStr, extraLogType, layer, inputs, file));
+                    name, dslStr, extraLogType, layer, inputs, file, lineNo));
             }
 
             if (!lalRules.isEmpty()) {
@@ -776,17 +783,42 @@ class LalComparisonTest {
         final String layer;
         final List<Map<String, Object>> inputs;
         final File sourceFile;
+        final int lineNo;
 
         LalRule(final String name, final String dsl,
                 final String extraLogType, final String layer,
                 final List<Map<String, Object>> inputs,
-                final File sourceFile) {
+                final File sourceFile, final int lineNo) {
             this.name = name;
             this.dsl = dsl;
             this.extraLogType = extraLogType;
             this.layer = layer;
             this.inputs = inputs;
             this.sourceFile = sourceFile;
+            this.lineNo = lineNo;
         }
     }
+
+    /**
+     * Find the 1-based line number of the Nth occurrence of {@code name: 
<value>} in YAML.
+     */
+    private static int findRuleLine(final String[] lines, final String name,
+                                    final int occurrence) {
+        int found = 0;
+        for (int i = 0; i < lines.length; i++) {
+            String trimmed = lines[i].trim();
+            if (trimmed.startsWith("- ")) {
+                trimmed = trimmed.substring(2);
+            }
+            if (trimmed.equals("name: " + name)
+                    || trimmed.equals("name: '" + name + "'")
+                    || trimmed.equals("name: \"" + name + "\"")) {
+                found++;
+                if (found == occurrence) {
+                    return i + 1;
+                }
+            }
+        }
+        return 0;
+    }
 }
diff --git 
a/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalComparisonTest.java
 
b/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalComparisonTest.java
index 1f84691350..9ba74a3d16 100644
--- 
a/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalComparisonTest.java
+++ 
b/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalComparisonTest.java
@@ -130,9 +130,29 @@ class MalComparisonTest {
         final List<DynamicTest> tests = new ArrayList<>();
         final Map<String, List<MalRule>> yamlRules = loadAllMalYamlFiles();
 
+        // Compile v2 filter once per source file — generates .class into 
generated-classes/
+        final java.util.Set<File> compiledFilters = new java.util.HashSet<>();
+
         for (final Map.Entry<String, List<MalRule>> entry : 
yamlRules.entrySet()) {
             final String yamlFile = entry.getKey();
             for (final MalRule rule : entry.getValue()) {
+                // Compile v2 filter once per source file (generates .class 
file)
+                if (rule.filter != null && rule.sourceFile != null
+                        && compiledFilters.add(rule.sourceFile)) {
+                    final String baseName = rule.sourceFile.getName()
+                        .replaceFirst("\\.(yaml|yml)$", "");
+                    final int filterLine = findFilterLine(rule.sourceFile);
+                    final MALClassGenerator filterGen = new 
MALClassGenerator();
+                    filterGen.setClassOutputDir(new java.io.File(
+                        rule.sourceFile.getParent(),
+                        baseName + ".generated-classes"));
+                    filterGen.setClassNameHint("filter");
+                    filterGen.setYamlSource(filterLine > 0
+                        ? rule.sourceFile.getName() + ":" + filterLine
+                        : rule.sourceFile.getName());
+                    filterGen.compileFilter(rule.filter);
+                }
+
                 // Compile v2 once per metric — compilation is independent of 
input data
                 org.apache.skywalking.oap.meter.analyzer.v2.dsl.MalExpression 
v2Expr = null;
                 ExpressionMetadata v2Meta = null;
@@ -167,6 +187,9 @@ class MalComparisonTest {
                 rule.sourceFile.getParent(),
                 baseName + ".generated-classes"));
             generator.setClassNameHint(rule.name);
+            generator.setYamlSource(rule.lineNo > 0
+                ? rule.sourceFile.getName() + ":" + rule.lineNo
+                : rule.sourceFile.getName());
         }
         return generator.compile(rule.name, rule.fullExpression);
     }
@@ -223,7 +246,8 @@ class MalComparisonTest {
                 (Map<String, Object>) rule.inputConfig.get("expected");
             if (inputSection != null) {
                 compareExecutionWithInput(
-                    rule, v1Expr, v2MalExpr, v2Meta, inputSection, 
expectedSection);
+                    rule, v1Expr, v2MalExpr, v2Meta,
+                    inputSection, expectedSection);
                 return;
             }
         }
@@ -1007,6 +1031,9 @@ class MalComparisonTest {
             final Object rawMetricPrefix = config.get("metricPrefix");
             final String metricPrefix = rawMetricPrefix instanceof String
                 ? (String) rawMetricPrefix : null;
+            final Object rawFilter = config.get("filter");
+            final String filter = rawFilter instanceof String
+                ? ((String) rawFilter).trim() : null;
             // Support both "metricsRules" (standard) and "metrics" (zabbix)
             List<Map<String, String>> rules =
                 (List<Map<String, String>>) config.get("metricsRules");
@@ -1029,6 +1056,8 @@ class MalComparisonTest {
             final String yamlName = prefix + "/" + file.getName();
             final List<MalRule> malRules = new ArrayList<>();
             final Map<String, Integer> nameCount = new HashMap<>();
+            // Build line number index: find "name: <value>" lines
+            final String[] lines = content.split("\n");
             for (final Map<String, String> rule : rules) {
                 final String name = rule.get("name");
                 final String exp = rule.get("exp");
@@ -1039,7 +1068,14 @@ class MalComparisonTest {
                 final int count = nameCount.merge(name, 1, Integer::sum);
                 final String uniqueName = count > 1 ? name + "_" + count : 
name;
                 final String fullExp = formatExp(expPrefix, expSuffix, exp);
-                malRules.add(new MalRule(uniqueName, fullExp, inputConfig, 
metricPrefix, file));
+                // Extract top-level dir name (e.g., "test-otel-rules") from 
prefix
+                final String dirName = prefix.contains("/")
+                    ? prefix.substring(0, prefix.indexOf('/')) : prefix;
+                // Find line number of this rule's "name:" in YAML
+                final int lineNo = findRuleLine(lines, name, count);
+                malRules.add(new MalRule(
+                    uniqueName, fullExp, inputConfig, metricPrefix,
+                    file, filter, dirName, lineNo));
             }
             if (!malRules.isEmpty()) {
                 result.put(yamlName, malRules);
@@ -1061,6 +1097,46 @@ class MalComparisonTest {
         return null;
     }
 
+    /**
+     * Find the 1-based line number of the {@code filter:} field in a YAML 
file.
+     */
+    private static int findFilterLine(final File yamlFile) {
+        try {
+            final String[] lines = 
Files.readString(yamlFile.toPath()).split("\n");
+            for (int i = 0; i < lines.length; i++) {
+                if (lines[i].trim().startsWith("filter:")) {
+                    return i + 1;
+                }
+            }
+        } catch (Exception ignored) {
+        }
+        return 0;
+    }
+
+    /**
+     * Find the 1-based line number of the Nth occurrence of {@code name: 
<value>} in the YAML.
+     */
+    private static int findRuleLine(final String[] lines, final String name,
+                                    final int occurrence) {
+        int found = 0;
+        for (int i = 0; i < lines.length; i++) {
+            String trimmed = lines[i].trim();
+            // Strip YAML list prefix "- "
+            if (trimmed.startsWith("- ")) {
+                trimmed = trimmed.substring(2);
+            }
+            if (trimmed.equals("name: " + name)
+                    || trimmed.equals("name: '" + name + "'")
+                    || trimmed.equals("name: \"" + name + "\"")) {
+                found++;
+                if (found == occurrence) {
+                    return i + 1;
+                }
+            }
+        }
+        return 0;
+    }
+
     /**
      * Replicates the production {@code MetricConvert.formatExp()} logic:
      * inserts {@code expPrefix} after the metric name (first dot-segment),
@@ -1093,15 +1169,22 @@ class MalComparisonTest {
         final Map<String, Object> inputConfig;
         final String metricPrefix;
         final File sourceFile;
+        final String filter;
+        final String dirName;
+        final int lineNo;
 
         MalRule(final String name, final String fullExpression,
                 final Map<String, Object> inputConfig, final String 
metricPrefix,
-                final File sourceFile) {
+                final File sourceFile, final String filter, final String 
dirName,
+                final int lineNo) {
             this.name = name;
             this.fullExpression = fullExpression;
             this.inputConfig = inputConfig;
             this.metricPrefix = metricPrefix;
             this.sourceFile = sourceFile;
+            this.filter = filter;
+            this.dirName = dirName;
+            this.lineNo = lineNo;
         }
     }
 }
diff --git 
a/test/script-cases/scripts/lal/test-lal/feature-cases/execution-basic.input.data
 
b/test/script-cases/scripts/lal/test-lal/feature-cases/execution-basic.data.yaml
similarity index 100%
rename from 
test/script-cases/scripts/lal/test-lal/feature-cases/execution-basic.input.data
rename to 
test/script-cases/scripts/lal/test-lal/feature-cases/execution-basic.data.yaml
diff --git 
a/test/script-cases/scripts/lal/test-lal/oap-cases/default.input.data 
b/test/script-cases/scripts/lal/test-lal/oap-cases/default.data.yaml
similarity index 100%
rename from test/script-cases/scripts/lal/test-lal/oap-cases/default.input.data
rename to test/script-cases/scripts/lal/test-lal/oap-cases/default.data.yaml
diff --git 
a/test/script-cases/scripts/lal/test-lal/oap-cases/envoy-als.input.data 
b/test/script-cases/scripts/lal/test-lal/oap-cases/envoy-als.data.yaml
similarity index 100%
rename from 
test/script-cases/scripts/lal/test-lal/oap-cases/envoy-als.input.data
rename to test/script-cases/scripts/lal/test-lal/oap-cases/envoy-als.data.yaml
diff --git 
a/test/script-cases/scripts/lal/test-lal/oap-cases/k8s-service.input.data 
b/test/script-cases/scripts/lal/test-lal/oap-cases/k8s-service.data.yaml
similarity index 100%
rename from 
test/script-cases/scripts/lal/test-lal/oap-cases/k8s-service.input.data
rename to test/script-cases/scripts/lal/test-lal/oap-cases/k8s-service.data.yaml
diff --git 
a/test/script-cases/scripts/lal/test-lal/oap-cases/mesh-dp.input.data 
b/test/script-cases/scripts/lal/test-lal/oap-cases/mesh-dp.data.yaml
similarity index 100%
rename from test/script-cases/scripts/lal/test-lal/oap-cases/mesh-dp.input.data
rename to test/script-cases/scripts/lal/test-lal/oap-cases/mesh-dp.data.yaml
diff --git 
a/test/script-cases/scripts/lal/test-lal/oap-cases/mysql-slowsql.input.data 
b/test/script-cases/scripts/lal/test-lal/oap-cases/mysql-slowsql.data.yaml
similarity index 100%
rename from 
test/script-cases/scripts/lal/test-lal/oap-cases/mysql-slowsql.input.data
rename to 
test/script-cases/scripts/lal/test-lal/oap-cases/mysql-slowsql.data.yaml
diff --git 
a/test/script-cases/scripts/lal/test-lal/oap-cases/network-profiling-e2e.input.data
 
b/test/script-cases/scripts/lal/test-lal/oap-cases/network-profiling-e2e.data.yaml
similarity index 100%
rename from 
test/script-cases/scripts/lal/test-lal/oap-cases/network-profiling-e2e.input.data
rename to 
test/script-cases/scripts/lal/test-lal/oap-cases/network-profiling-e2e.data.yaml
diff --git a/test/script-cases/scripts/lal/test-lal/oap-cases/nginx.input.data 
b/test/script-cases/scripts/lal/test-lal/oap-cases/nginx.data.yaml
similarity index 100%
rename from test/script-cases/scripts/lal/test-lal/oap-cases/nginx.input.data
rename to test/script-cases/scripts/lal/test-lal/oap-cases/nginx.data.yaml
diff --git 
a/test/script-cases/scripts/lal/test-lal/oap-cases/pgsql-slowsql.input.data 
b/test/script-cases/scripts/lal/test-lal/oap-cases/pgsql-slowsql.data.yaml
similarity index 100%
rename from 
test/script-cases/scripts/lal/test-lal/oap-cases/pgsql-slowsql.input.data
rename to 
test/script-cases/scripts/lal/test-lal/oap-cases/pgsql-slowsql.data.yaml
diff --git 
a/test/script-cases/scripts/lal/test-lal/oap-cases/redis-slowsql.input.data 
b/test/script-cases/scripts/lal/test-lal/oap-cases/redis-slowsql.data.yaml
similarity index 100%
rename from 
test/script-cases/scripts/lal/test-lal/oap-cases/redis-slowsql.input.data
rename to 
test/script-cases/scripts/lal/test-lal/oap-cases/redis-slowsql.data.yaml

Reply via email to