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

wu-sheng 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 dcfb07014e Fix custom layers: MAL v2 Layer.NAME compilation + 
layer-extensions.yml jar shadowing (#13918)
dcfb07014e is described below

commit dcfb07014ea5b6813aed214ba34e6230678d37a3
Author: Marc Navarro <[email protected]>
AuthorDate: Fri Jun 19 08:36:22 2026 +0200

    Fix custom layers: MAL v2 Layer.NAME compilation + layer-extensions.yml jar 
shadowing (#13918)
    
    **Bug 1 — MAL v2 compiler cannot compile a custom-layer reference**
    
    A custom layer has no generated `Layer.*` static field — it lives only in 
the name registry — so referencing it in a MAL expression failed v2 code 
generation. The natural form `service(['svc'], Layer.IOT_FLEET)` parses as the 
static-field `enumRef` (`IDENTIFIER DOT IDENTIFIER`, exactly like 
`Layer.GENERAL`), and codegen emitted it verbatim as a `Layer.IOT_FLEET` field 
access — a field that does not exist on `Layer` — so Javassist failed to 
compile the generated source.
    
    Fix: `MALMethodChainCodegen` now lowers every `Layer.NAME` static-field 
reference to a runtime `Layer.nameOf("NAME")` registry lookup, scoped to 
`Layer` only (via `MALCodegenHelper.LAYER_ENUM_TYPE`). `Layer` is a 
registry-backed type, not a Java enum, and `Layer.nameOf` resolves both 
built-in and custom layers by name; for a built-in this is behavior-preserving, 
because `Layer.nameOf("GENERAL")` returns the same instance as the 
`Layer.GENERAL` field. The other `ENUM_FQCN` types (`Dete [...]
    
    Tests (`MALClassGeneratorTest`): a custom-layer field form lowers to 
`Layer.nameOf("...")` and compiles; a built-in field form lowers identically; a 
non-`Layer` enum (`DetectPoint`) keeps its direct field access. All 1350 
bundled-rule execution tests pass — every built-in `Layer.*` reference now 
routes through `nameOf` and resolves to the same layer (no rule used a custom 
layer before, which is why CI never caught it).
    
    > _Note: an earlier revision of this PR fixed only the explicit 
`Layer.nameOf('NAME')` method form via a parser reinterpretation 
(`EnumStaticCallArgument`). That was superseded by the field-form lowering 
above — the field form is the natural, documented-as-built-in syntax and needs 
no grammar/AST special-casing. Neither form had shipped, so there is no 
compatibility concern._
    
    **Bug 2 — `layer-extensions.yml` bundled in the jar shadows the operator's 
`config/` copy**
    
    `layer-extensions.yml` (operator-managed, documented as living in 
`config/`) was packaged inside `skywalking-oap.jar` and not shipped to the 
distribution `config/`. The launch script 
(`dist-material/bin/oapService.sh:38-41`) prepends every `oap-libs/*.jar` to 
`CLASSPATH`, leaving `config/` last, so 
`ResourceUtils.read("layer-extensions.yml")` → 
`getClassLoader().getResource(...)` deterministically resolved the empty 
jar-bundled template (`layers: []`) and silently ignored the operator [...]
    
    Fix: add `layer-extensions.yml` to the `maven-jar-plugin` `<excludes>` 
(`oap-server/server-starter/pom.xml`) and to the assembly `<includes>` 
(`apm-dist/src/main/assembly/binary.xml`), so it follows the same 
exclude-from-jar + copy-to-`config/` packaging as every other operator-editable 
config. Verified by rebuilding `skywalking-oap.jar` and confirming the file is 
no longer present.
---
 apm-dist/src/main/assembly/binary.xml              |  1 +
 docs/en/changes/changes.md                         |  2 +
 docs/en/concepts-and-designs/mal.md                |  7 ++-
 .../analyzer/v2/compiler/MALCodegenHelper.java     |  9 ++++
 .../v2/compiler/MALMethodChainCodegen.java         | 16 ++++--
 .../v2/compiler/MALClassGeneratorTest.java         | 59 ++++++++++++++++++++++
 oap-server/server-starter/pom.xml                  |  1 +
 7 files changed, 90 insertions(+), 5 deletions(-)

diff --git a/apm-dist/src/main/assembly/binary.xml 
b/apm-dist/src/main/assembly/binary.xml
index b392cb39ef..47451e9559 100644
--- a/apm-dist/src/main/assembly/binary.xml
+++ b/apm-dist/src/main/assembly/binary.xml
@@ -74,6 +74,7 @@
                 <include>telegraf-rules/*</include>
                 <include>cilium-rules/*</include>
                 <include>gen-ai-config.yml</include>
+                <include>layer-extensions.yml</include>
             </includes>
             <outputDirectory>config</outputDirectory>
         </fileSet>
diff --git a/docs/en/changes/changes.md b/docs/en/changes/changes.md
index 7658d17f1d..19944574a6 100644
--- a/docs/en/changes/changes.md
+++ b/docs/en/changes/changes.md
@@ -307,6 +307,8 @@
 * Fix: continuous profiling policy validation now rejects a threshold / count 
of `0` to match the error messages and rover's `value >= threshold` trigger 
semantics (a `0` threshold would always trigger). CPU percent and HTTP error 
rate are tightened from `[0-100]` to `(0-100]`.
 * Fix wrong BanyanDB resource options in record data.
 * Align the default BanyanDB stage `segmentInterval` values so each coarser 
stage is an integer multiple of the finer one (`records` cold `3` → `4`, 
`metricsMinute` cold `5` → `6`, `metricsHour` warm `7` → `10` and cold `15` → 
`20`), keeping hot → warm → cold lifecycle migration on the cheap whole-segment 
fast path.
+* Fix: `layer-extensions.yml` is now excluded from the `skywalking-oap` jar 
and shipped to the distribution `config/` directory, so an operator-edited 
`config/layer-extensions.yml` is no longer shadowed by the empty template 
bundled in the jar. Because the OAP launch script puts `oap-libs/*.jar` ahead 
of `config/` on the classpath, `ResourceUtils.read("layer-extensions.yml")` 
previously always resolved the jar-bundled `layers: []` and silently ignored 
the operator's file — custom layers  [...]
+* Fix: the v2 MAL compiler now resolves custom layers referenced as 
`Layer.NAME` in an expression. A custom layer declared through a 
`layerDefinitions:` block (or `layer-extensions.yml` / the `LayerExtension` 
SPI) has no generated `Layer.*` static field, so `service(['svc'], 
Layer.IOT_FLEET)` previously failed code generation because `Layer` has no 
`IOT_FLEET` field. The compiler now lowers every `Layer.NAME` static-field 
reference to a runtime `Layer.nameOf("NAME")` registry lookup, so  [...]
 
 #### UI
 * Add Airflow layer dashboards and menu i18n under Workflow Scheduler in 
Horizon UI (SWIP-7).
diff --git a/docs/en/concepts-and-designs/mal.md 
b/docs/en/concepts-and-designs/mal.md
index 5d8c92be2e..2387cf6ffa 100644
--- a/docs/en/concepts-and-designs/mal.md
+++ b/docs/en/concepts-and-designs/mal.md
@@ -393,10 +393,15 @@ layerDefinitions:
 metricsRules:
   - name: device_battery_percentage
     exp: iot_device_battery_level.tagAverage(['service'], ['host'])
-expSuffix: instance(['host'], ['service'], Layer.nameOf('IOT_FLEET'))
+expSuffix: instance(['host'], ['service'], Layer.IOT_FLEET)
 ```
 
 Notes:
+- **Reference a custom layer like a built-in one.** Write `Layer.IOT_FLEET` 
exactly as you would a
+  built-in such as `Layer.GENERAL` — the compiler lowers every `Layer.<NAME>` 
reference to a runtime
+  registry lookup, so a custom layer needs no generated static field. (Because 
the lookup is by name,
+  a misspelled name resolves to `Layer.UNDEFINED` at runtime rather than 
failing to compile, so keep
+  the name in `layerDefinitions:` and the expression in sync.)
 - **Storage encoding is the ordinal int**, persisted in BanyanDB / 
Elasticsearch / JDBC. Every
   OAP node that reads or writes a given layer must agree on its `(name, 
ordinal)` mapping —
   deploy a MAL file with `layerDefinitions:` identically across all nodes.
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 948f6a949b..428b230a76 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
@@ -39,6 +39,15 @@ final class MALCodegenHelper {
 
     // ---- Well-known enum types used in MAL expressions ----
 
+    /**
+     * Short name of the {@code Layer} type as written in MAL expressions. 
Unlike the other
+     * {@link #ENUM_FQCN} entries (real Java enums), {@code Layer} is a 
registry-backed type
+     * whose custom members have no static field, so a {@code Layer.NAME} 
reference is lowered
+     * to a runtime {@code Layer.nameOf("NAME")} lookup in code generation 
rather than a
+     * static-field access — see {@code 
MALMethodChainCodegen#generateArgument}.
+     */
+    static final String LAYER_ENUM_TYPE = "Layer";
+
     static final Map<String, String> ENUM_FQCN;
 
     // ---- Well-known helper classes used inside MAL closures ----
diff --git 
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALMethodChainCodegen.java
 
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALMethodChainCodegen.java
index d707c0a49e..7a2b4bc87c 100644
--- 
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALMethodChainCodegen.java
+++ 
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALMethodChainCodegen.java
@@ -321,12 +321,20 @@ final class MALMethodChainCodegen {
                 (MALExpressionModel.EnumRefArgument) arg;
             final String fqcn =
                 MALCodegenHelper.ENUM_FQCN.get(enumRef.getEnumType());
-            if (fqcn != null) {
-                sb.append(fqcn);
+            final String typeRef = fqcn != null ? fqcn : enumRef.getEnumType();
+            if 
(MALCodegenHelper.LAYER_ENUM_TYPE.equals(enumRef.getEnumType())) {
+                // Layer is a registry-backed type, not a Java enum. A custom 
layer
+                // (layerDefinitions: / layer-extensions.yml / SPI / runtime 
hot-update) has
+                // no Layer.* static field, so every Layer.NAME reference is 
lowered to a
+                // runtime Layer.nameOf("NAME") lookup. For a built-in layer 
this is
+                // equivalent — Layer.nameOf("GENERAL") returns the same 
instance as the
+                // Layer.GENERAL field. Other ENUM_FQCN types are real enums 
with no
+                // nameOf(String), so they keep the direct static-field 
reference below.
+                sb.append(typeRef).append(".nameOf(\"")
+                  .append(enumRef.getEnumValue()).append("\")");
             } else {
-                sb.append(enumRef.getEnumType());
+                sb.append(typeRef).append('.').append(enumRef.getEnumValue());
             }
-            sb.append('.').append(enumRef.getEnumValue());
         } else if (arg instanceof MALExpressionModel.ExprArgument) {
             final MALExpressionModel.Expr innerExpr =
                 ((MALExpressionModel.ExprArgument) arg).getExpr();
diff --git 
a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorTest.java
 
b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorTest.java
index f6594c66e8..785adc3038 100644
--- 
a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorTest.java
+++ 
b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorTest.java
@@ -27,6 +27,7 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -134,6 +135,64 @@ class MALClassGeneratorTest {
         assertTrue(source.contains("getOrDefault"));
     }
 
+    @Test
+    void compileCustomLayerViaStaticFieldForm() throws Exception {
+        // A custom layer may also be referenced with the ordinary 
static-field syntax
+        // (Layer.IOT_FLEET), exactly like a built-in. Because the layer has 
no generated
+        // static field, codegen must lower it to a runtime 
Layer.nameOf("IOT_FLEET") lookup
+        // rather than emitting a Layer.IOT_FLEET field access that cannot 
compile.
+        final String source = generator.generateSource(
+            "instance_jvm_cpu.sum(['service']).service(['service'], 
Layer.IOT_FLEET)");
+        assertNotNull(source);
+        assertTrue(
+            source.contains(
+                
"org.apache.skywalking.oap.server.core.analysis.Layer.nameOf(\"IOT_FLEET\")"),
+            "Custom-layer static-field form should be lowered to 
Layer.nameOf(...): " + source);
+        assertNotNull(generator.compile(
+            "test_custom_layer_field_form",
+            "instance_jvm_cpu.sum(['service']).service(['service'], 
Layer.IOT_FLEET)"));
+    }
+
+    @Test
+    void builtInLayerStaticFieldFormLoweredToNameOf() throws Exception {
+        // Built-in layers are lowered the same way as custom ones: 
Layer.GENERAL becomes
+        // Layer.nameOf("GENERAL"), which returns the identical instance as 
the Layer.GENERAL
+        // static field, so the rewrite is behavior-preserving and needs no 
built-in/custom
+        // branch in codegen.
+        final String source = generator.generateSource(
+            "instance_jvm_cpu.sum(['service']).service(['service'], 
Layer.GENERAL)");
+        assertNotNull(source);
+        assertTrue(
+            source.contains(
+                
"org.apache.skywalking.oap.server.core.analysis.Layer.nameOf(\"GENERAL\")"),
+            "Built-in layer static-field form should be lowered to 
Layer.nameOf(...): " + source);
+        assertNotNull(generator.compile(
+            "test_builtin_layer_field_form",
+            "instance_jvm_cpu.sum(['service']).service(['service'], 
Layer.GENERAL)"));
+    }
+
+    @Test
+    void nonLayerEnumStaticFieldFormKeepsDirectReference() throws Exception {
+        // The Layer.nameOf(...) lowering is scoped to Layer only. Real Java 
enums such as
+        // DetectPoint have no nameOf(String), so their static-field 
references must stay
+        // direct field accesses or the generated source would not compile. 
This expression
+        // also carries a Layer ref, confirming the two are lowered 
differently side by side.
+        final String source = generator.generateSource(
+            "service_relation_req.sum(['client', 'server']).serviceRelation("
+                + "DetectPoint.SERVER, ['client'], ['server'], '|', 
Layer.GENERAL, 'component')");
+        assertNotNull(source);
+        assertTrue(
+            source.contains("DetectPoint.SERVER"),
+            "Non-Layer enum should keep the direct static-field reference: " + 
source);
+        assertFalse(
+            source.contains("DetectPoint.nameOf"),
+            "Non-Layer enum must NOT be lowered to a nameOf(...) lookup: " + 
source);
+        assertTrue(
+            source.contains(
+                
"org.apache.skywalking.oap.server.core.analysis.Layer.nameOf(\"GENERAL\")"),
+            "Layer ref in the same expression should still be lowered to 
nameOf(...): " + source);
+    }
+
     @Test
     void filterSafeNavCompiles() throws Exception {
         final String source = generator.generateFilterSource(
diff --git a/oap-server/server-starter/pom.xml 
b/oap-server/server-starter/pom.xml
index 8fef6d229f..70eaf6aec0 100644
--- a/oap-server/server-starter/pom.xml
+++ b/oap-server/server-starter/pom.xml
@@ -377,6 +377,7 @@
                         <exclude>telegraf-rules/</exclude>
                         <exclude>cilium-rules/</exclude>
                         <exclude>gen-ai-config.yml</exclude>
+                        <exclude>layer-extensions.yml</exclude>
                     </excludes>
                 </configuration>
             </plugin>

Reply via email to