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