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

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

commit cae90d5a1042f3b2c8fc4d7a788aa37a538eba7b
Author: Wu Sheng <[email protected]>
AuthorDate: Mon Mar 2 10:35:46 2026 +0800

    Support regex match, def type inference, GString interpolation, and time() 
scalar in MAL compiler
    
    Add ANTLR4 lexer mode for regex literals (=~ /pattern/), def keyword with
    type inference from initializer (String[][] for regex, String[] for split),
    GString interpolation expansion, .size() to .length translation, decorate()
    bean-mode closures, and time() as a scalar function in binary expressions.
    Verified with 1,228 v1-v2 checker tests (1,197 MAL + 31 filter).
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../apache/skywalking/mal/rt/grammar/MALLexer.g4   |  21 ++
 .../apache/skywalking/mal/rt/grammar/MALParser.g4  |  14 +-
 .../analyzer/v2/compiler/MALClassGenerator.java    | 334 +++++++++++++++++----
 .../analyzer/v2/compiler/MALExpressionModel.java   |  40 +++
 .../analyzer/v2/compiler/MALScriptParser.java      | 210 ++++++++++++-
 .../analyzer/v2/compiler/rt/MalRuntimeHelper.java  |  24 ++
 .../v2/compiler/MALClassGeneratorTest.java         |  41 +++
 .../analyzer/v2/compiler/MALScriptParserTest.java  |  47 +++
 .../mal/test-envoy-metrics-rules/envoy-ca.yaml     |  60 ++++
 .../satellite-tag-prefix.yaml                      |  36 +++
 .../service-decorate-attributes.yaml               |  40 +++
 .../service-gstring-regex-split.yaml               |  20 ++
 12 files changed, 826 insertions(+), 61 deletions(-)

diff --git 
a/oap-server/analyzer/meter-analyzer/src/main/antlr4/org/apache/skywalking/mal/rt/grammar/MALLexer.g4
 
b/oap-server/analyzer/meter-analyzer/src/main/antlr4/org/apache/skywalking/mal/rt/grammar/MALLexer.g4
index 2466d484d6..b8958b2916 100644
--- 
a/oap-server/analyzer/meter-analyzer/src/main/antlr4/org/apache/skywalking/mal/rt/grammar/MALLexer.g4
+++ 
b/oap-server/analyzer/meter-analyzer/src/main/antlr4/org/apache/skywalking/mal/rt/grammar/MALLexer.g4
@@ -53,7 +53,11 @@ GTE:        '>=';
 LTE:        '<=';
 NOT:        '!';
 
+// Regex match operator: switches to REGEX_MODE to lex the pattern
+REGEX_MATCH: '=~' -> pushMode(REGEX_MODE);
+
 // Keywords
+DEF:        'def';
 IF:         'if';
 ELSE:       'else';
 RETURN:     'return';
@@ -109,3 +113,20 @@ fragment LetterOrDigit
     : Letter
     | [0-9]
     ;
+
+// ==================== Regex mode ====================
+// Activated after '=~', lexes a /pattern/ regex literal, then pops back.
+mode REGEX_MODE;
+
+REGEX_WS
+    : [ \t\r\n]+ -> channel(HIDDEN)
+    ;
+
+REGEX_LITERAL
+    : '/' RegexBodyChar+ '/'  -> popMode
+    ;
+
+fragment RegexBodyChar
+    : '\\' .         // escaped character (e.g. \. \( \[ )
+    | ~[/\r\n]       // anything except / and newline
+    ;
diff --git 
a/oap-server/analyzer/meter-analyzer/src/main/antlr4/org/apache/skywalking/mal/rt/grammar/MALParser.g4
 
b/oap-server/analyzer/meter-analyzer/src/main/antlr4/org/apache/skywalking/mal/rt/grammar/MALParser.g4
index fac507dc01..53f4d3e7d0 100644
--- 
a/oap-server/analyzer/meter-analyzer/src/main/antlr4/org/apache/skywalking/mal/rt/grammar/MALParser.g4
+++ 
b/oap-server/analyzer/meter-analyzer/src/main/antlr4/org/apache/skywalking/mal/rt/grammar/MALParser.g4
@@ -146,8 +146,12 @@ closureStatement
 
 // ==================== Variable declarations ====================
 // Groovy-style: String result = "", String protocol = tags['protocol']
+// Also supports array types: String[] parts = ...
+// Also supports def keyword: def matcher = ...
 variableDeclaration
-    : IDENTIFIER IDENTIFIER ASSIGN closureExpr SEMI?
+    : IDENTIFIER L_BRACKET R_BRACKET IDENTIFIER ASSIGN closureExpr SEMI?
+    | IDENTIFIER IDENTIFIER ASSIGN closureExpr SEMI?
+    | DEF IDENTIFIER ASSIGN closureExpr SEMI?
     ;
 
 // ==================== Closure statements ====================
@@ -202,8 +206,10 @@ closureConditionPrimary
     ;
 
 closureExpr
-    : closureExpr QUESTION closureExpr COLON closureExpr           # 
closureTernary
+    : closureExpr compOp closureExpr QUESTION closureExpr COLON closureExpr   
# closureTernaryComp
+    | closureExpr QUESTION closureExpr COLON closureExpr           # 
closureTernary
     | closureExpr QUESTION COLON closureExpr                       # 
closureElvis
+    | closureExpr REGEX_MATCH REGEX_LITERAL                        # 
closureRegexMatch
     | closureExpr PLUS closureExpr                                 # closureAdd
     | closureExpr MINUS closureExpr                                # closureSub
     | closureExpr STAR closureExpr                                 # closureMul
@@ -258,6 +264,10 @@ closureArgList
     : closureExpr (COMMA closureExpr)*
     ;
 
+compOp
+    : GT | LT | GTE | LTE | DEQ | NEQ
+    ;
+
 closureFieldAccess
     : IDENTIFIER (DOT IDENTIFIER)* (L_BRACKET closureExpr R_BRACKET)?
     ;
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 9f2940a149..a057020d2e 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
@@ -298,6 +298,8 @@ public final class MALClassGenerator {
                 } else if ("instance".equals(methodName)) {
                     interfaceType = 
"org.apache.skywalking.oap.meter.analyzer.v2.dsl"
                         + ".SampleFamilyFunctions$PropertiesExtractor";
+                } else if ("decorate".equals(methodName)) {
+                    interfaceType = DECORATE_FUNCTION_TYPE;
                 } else {
                     interfaceType = 
"org.apache.skywalking.oap.meter.analyzer.v2.dsl"
                         + ".SampleFamilyFunctions$TagFunction";
@@ -320,6 +322,15 @@ public final class MALClassGenerator {
     private static final String PROPERTIES_EXTRACTOR_TYPE =
         
"org.apache.skywalking.oap.meter.analyzer.v2.dsl.SampleFamilyFunctions$PropertiesExtractor";
 
+    private static final String DECORATE_FUNCTION_TYPE =
+        
"org.apache.skywalking.oap.meter.analyzer.v2.dsl.SampleFamilyFunctions$DecorateFunction";
+
+    private static final String METER_ENTITY_FQCN =
+        "org.apache.skywalking.oap.server.core.analysis.meter.MeterEntity";
+
+    private static final String RUNTIME_HELPER_FQCN =
+        
"org.apache.skywalking.oap.meter.analyzer.v2.compiler.rt.MalRuntimeHelper";
+
     private Object compileClosureClass(final String className,
                                        final ClosureInfo info) throws 
Exception {
         final CtClass ctClass = classPool.makeClass(className);
@@ -389,6 +400,25 @@ public final class MALClassGenerator {
             ctClass.addMethod(CtNewMethod.make(
                 "public Object apply(Object o) { return apply((java.util.Map) 
o); }",
                 ctClass));
+        } else if (DECORATE_FUNCTION_TYPE.equals(info.interfaceType)) {
+            // DecorateFunction: void accept(MeterEntity)
+            // Closure param operates on MeterEntity bean properties 
(getters/setters).
+            final String paramName = params.isEmpty() ? "it" : params.get(0);
+
+            final StringBuilder sb = new StringBuilder();
+            sb.append("public void accept(Object _arg) {\n");
+            sb.append("  ").append(METER_ENTITY_FQCN).append(" ")
+              .append(paramName).append(" = (").append(METER_ENTITY_FQCN)
+              .append(") _arg;\n");
+            for (final MALExpressionModel.ClosureStatement stmt : 
closure.getBody()) {
+                generateClosureStatement(sb, stmt, paramName, true);
+            }
+            sb.append("}\n");
+
+            if (log.isDebugEnabled()) {
+                log.debug("Decorate closure body:\n{}", sb);
+            }
+            ctClass.addMethod(CtNewMethod.make(sb.toString(), ctClass));
         } else {
             // TagFunction: Map<String,String> apply(Map<String,String> tags)
             final String paramName = params.isEmpty() ? "it" : params.get(0);
@@ -503,32 +533,40 @@ public final class MALClassGenerator {
         final MALExpressionModel.Expr right = expr.getRight();
         final MALExpressionModel.ArithmeticOp op = expr.getOp();
 
-        final boolean leftIsNumber = left instanceof 
MALExpressionModel.NumberExpr;
-        final boolean rightIsNumber = right instanceof 
MALExpressionModel.NumberExpr;
+        final boolean leftIsNumber = left instanceof 
MALExpressionModel.NumberExpr
+            || isScalarFunction(left);
+        final boolean rightIsNumber = right instanceof 
MALExpressionModel.NumberExpr
+            || isScalarFunction(right);
 
         if (leftIsNumber && !rightIsNumber) {
             // N op SF -> swap to SF.op(N) with special handling for SUB and 
DIV
-            final double num = ((MALExpressionModel.NumberExpr) 
left).getValue();
             switch (op) {
                 case ADD:
                     sb.append("(");
                     generateExpr(sb, right);
-                    
sb.append(").plus(Double.valueOf(").append(num).append("))");
+                    sb.append(").plus(Double.valueOf(");
+                    generateScalarExpr(sb, left);
+                    sb.append("))");
                     break;
                 case SUB:
                     sb.append("(");
                     generateExpr(sb, right);
-                    sb.append(").minus(Double.valueOf(")
-                      .append(num).append(")).negative()");
+                    sb.append(").minus(Double.valueOf(");
+                    generateScalarExpr(sb, left);
+                    sb.append(")).negative()");
                     break;
                 case MUL:
                     sb.append("(");
                     generateExpr(sb, right);
-                    
sb.append(").multiply(Double.valueOf(").append(num).append("))");
+                    sb.append(").multiply(Double.valueOf(");
+                    generateScalarExpr(sb, left);
+                    sb.append("))");
                     break;
                 case DIV:
                     
sb.append("org.apache.skywalking.oap.meter.analyzer.v2.compiler.rt")
-                      
.append(".MalRuntimeHelper.divReverse(").append(num).append(", ");
+                      .append(".MalRuntimeHelper.divReverse(");
+                    generateScalarExpr(sb, left);
+                    sb.append(", ");
                     generateExpr(sb, right);
                     sb.append(")");
                     break;
@@ -537,11 +575,12 @@ public final class MALClassGenerator {
             }
         } else if (!leftIsNumber && rightIsNumber) {
             // SF op N
-            final double num = ((MALExpressionModel.NumberExpr) 
right).getValue();
             sb.append("(");
             generateExpr(sb, left);
             sb.append(").").append(opMethodName(op))
-              .append("(Double.valueOf(").append(num).append("))");
+              .append("(Double.valueOf(");
+            generateScalarExpr(sb, right);
+            sb.append("))");
         } else {
             // SF op SF (both non-number)
             sb.append("(");
@@ -712,28 +751,54 @@ public final class MALClassGenerator {
     private void generateClosureStatement(final StringBuilder sb,
                                           final 
MALExpressionModel.ClosureStatement stmt,
                                           final String paramName) {
+        generateClosureStatement(sb, stmt, paramName, false);
+    }
+
+    private void generateClosureStatement(final StringBuilder sb,
+                                          final 
MALExpressionModel.ClosureStatement stmt,
+                                          final String paramName,
+                                          final boolean beanMode) {
         if (stmt instanceof MALExpressionModel.ClosureAssignment) {
             final MALExpressionModel.ClosureAssignment assign =
                 (MALExpressionModel.ClosureAssignment) stmt;
-            sb.append("    ").append(assign.getMapVar()).append(".put(");
-            generateClosureExpr(sb, assign.getKeyExpr(), paramName);
-            sb.append(", ");
-            generateClosureExpr(sb, assign.getValue(), paramName);
-            sb.append(");\n");
+            if (beanMode) {
+                // Bean setter: me.attr0 = 'value' → me.setAttr0("value")
+                final String keyText = extractConstantKey(assign.getKeyExpr());
+                if (keyText != null) {
+                    sb.append("    ").append(assign.getMapVar()).append(".set")
+                      .append(Character.toUpperCase(keyText.charAt(0)))
+                      .append(keyText.substring(1)).append("(");
+                    generateClosureExpr(sb, assign.getValue(), paramName, 
beanMode);
+                    sb.append(");\n");
+                } else {
+                    // Fallback to map put for dynamic keys
+                    sb.append("    
").append(assign.getMapVar()).append(".put(");
+                    generateClosureExpr(sb, assign.getKeyExpr(), paramName, 
beanMode);
+                    sb.append(", ");
+                    generateClosureExpr(sb, assign.getValue(), paramName, 
beanMode);
+                    sb.append(");\n");
+                }
+            } else {
+                sb.append("    ").append(assign.getMapVar()).append(".put(");
+                generateClosureExpr(sb, assign.getKeyExpr(), paramName, 
beanMode);
+                sb.append(", ");
+                generateClosureExpr(sb, assign.getValue(), paramName, 
beanMode);
+                sb.append(");\n");
+            }
         } else if (stmt instanceof MALExpressionModel.ClosureIfStatement) {
             final MALExpressionModel.ClosureIfStatement ifStmt =
                 (MALExpressionModel.ClosureIfStatement) stmt;
             sb.append("    if (");
-            generateClosureCondition(sb, ifStmt.getCondition(), paramName);
+            generateClosureCondition(sb, ifStmt.getCondition(), paramName, 
beanMode);
             sb.append(") {\n");
             for (final MALExpressionModel.ClosureStatement s : 
ifStmt.getThenBranch()) {
-                generateClosureStatement(sb, s, paramName);
+                generateClosureStatement(sb, s, paramName, beanMode);
             }
             sb.append("    }\n");
             if (!ifStmt.getElseBranch().isEmpty()) {
                 sb.append("    else {\n");
                 for (final MALExpressionModel.ClosureStatement s : 
ifStmt.getElseBranch()) {
-                    generateClosureStatement(sb, s, paramName);
+                    generateClosureStatement(sb, s, paramName, beanMode);
                 }
                 sb.append("    }\n");
             }
@@ -741,11 +806,14 @@ public final class MALClassGenerator {
             final MALExpressionModel.ClosureReturnStatement retStmt =
                 (MALExpressionModel.ClosureReturnStatement) stmt;
             if (retStmt.getValue() == null) {
-                // Bare return (void return for ForEachFunction, or early exit)
                 sb.append("    return;\n");
             } else {
-                sb.append("    return (java.util.Map) ");
-                generateClosureExpr(sb, retStmt.getValue(), paramName);
+                if (beanMode) {
+                    sb.append("    return ");
+                } else {
+                    sb.append("    return (java.util.Map) ");
+                }
+                generateClosureExpr(sb, retStmt.getValue(), paramName, 
beanMode);
                 sb.append(";\n");
             }
         } else if (stmt instanceof MALExpressionModel.ClosureVarDecl) {
@@ -753,18 +821,19 @@ public final class MALClassGenerator {
                 (MALExpressionModel.ClosureVarDecl) stmt;
             sb.append("    ").append(vd.getTypeName()).append(" ")
               .append(vd.getVarName()).append(" = ");
-            generateClosureExpr(sb, vd.getInitializer(), paramName);
+            generateClosureExpr(sb, vd.getInitializer(), paramName, beanMode);
             sb.append(";\n");
         } else if (stmt instanceof MALExpressionModel.ClosureVarAssign) {
             final MALExpressionModel.ClosureVarAssign va =
                 (MALExpressionModel.ClosureVarAssign) stmt;
             sb.append("    ").append(va.getVarName()).append(" = ");
-            generateClosureExpr(sb, va.getValue(), paramName);
+            generateClosureExpr(sb, va.getValue(), paramName, beanMode);
             sb.append(";\n");
         } else if (stmt instanceof MALExpressionModel.ClosureExprStatement) {
             sb.append("    ");
             generateClosureExpr(sb,
-                ((MALExpressionModel.ClosureExprStatement) stmt).getExpr(), 
paramName);
+                ((MALExpressionModel.ClosureExprStatement) stmt).getExpr(), 
paramName,
+                beanMode);
             sb.append(";\n");
         }
     }
@@ -772,6 +841,13 @@ public final class MALClassGenerator {
     private void generateClosureExpr(final StringBuilder sb,
                                      final MALExpressionModel.ClosureExpr expr,
                                      final String paramName) {
+        generateClosureExpr(sb, expr, paramName, false);
+    }
+
+    private void generateClosureExpr(final StringBuilder sb,
+                                     final MALExpressionModel.ClosureExpr expr,
+                                     final String paramName,
+                                     final boolean beanMode) {
         if (expr instanceof MALExpressionModel.ClosureStringLiteral) {
             sb.append('"')
               .append(escapeJava(((MALExpressionModel.ClosureStringLiteral) 
expr).getValue()))
@@ -783,7 +859,6 @@ public final class MALClassGenerator {
         } else if (expr instanceof MALExpressionModel.ClosureNullLiteral) {
             sb.append("null");
         } else if (expr instanceof MALExpressionModel.ClosureMapLiteral) {
-            // Inline map construction using java.util.Map.of()
             final MALExpressionModel.ClosureMapLiteral mapLit =
                 (MALExpressionModel.ClosureMapLiteral) expr;
             sb.append("java.util.Map.of(");
@@ -793,17 +868,17 @@ public final class MALClassGenerator {
                 }
                 final MALExpressionModel.MapEntry entry = 
mapLit.getEntries().get(i);
                 sb.append('"').append(escapeJava(entry.getKey())).append("\", 
");
-                generateClosureExpr(sb, entry.getValue(), paramName);
+                generateClosureExpr(sb, entry.getValue(), paramName, beanMode);
             }
             sb.append(")");
         } else if (expr instanceof MALExpressionModel.ClosureMethodChain) {
             generateClosureMethodChain(sb,
-                (MALExpressionModel.ClosureMethodChain) expr, paramName);
+                (MALExpressionModel.ClosureMethodChain) expr, paramName, 
beanMode);
         } else if (expr instanceof MALExpressionModel.ClosureBinaryExpr) {
             final MALExpressionModel.ClosureBinaryExpr bin =
                 (MALExpressionModel.ClosureBinaryExpr) expr;
             sb.append("(");
-            generateClosureExpr(sb, bin.getLeft(), paramName);
+            generateClosureExpr(sb, bin.getLeft(), paramName, beanMode);
             switch (bin.getOp()) {
                 case ADD:
                     sb.append(" + ");
@@ -820,35 +895,52 @@ public final class MALClassGenerator {
                 default:
                     break;
             }
-            generateClosureExpr(sb, bin.getRight(), paramName);
+            generateClosureExpr(sb, bin.getRight(), paramName, beanMode);
+            sb.append(")");
+        } else if (expr instanceof MALExpressionModel.ClosureCompTernaryExpr) {
+            final MALExpressionModel.ClosureCompTernaryExpr ct =
+                (MALExpressionModel.ClosureCompTernaryExpr) expr;
+            sb.append("(");
+            generateClosureExpr(sb, ct.getLeft(), paramName, beanMode);
+            sb.append(comparisonOperator(ct.getOp()));
+            generateClosureExpr(sb, ct.getRight(), paramName, beanMode);
+            sb.append(" ? ");
+            generateClosureExpr(sb, ct.getTrueExpr(), paramName, beanMode);
+            sb.append(" : ");
+            generateClosureExpr(sb, ct.getFalseExpr(), paramName, beanMode);
             sb.append(")");
         } else if (expr instanceof MALExpressionModel.ClosureTernaryExpr) {
             final MALExpressionModel.ClosureTernaryExpr ternary =
                 (MALExpressionModel.ClosureTernaryExpr) expr;
             sb.append("(((Object)(");
-            generateClosureExpr(sb, ternary.getCondition(), paramName);
+            generateClosureExpr(sb, ternary.getCondition(), paramName, 
beanMode);
             sb.append(")) != null ? (");
-            generateClosureExpr(sb, ternary.getTrueExpr(), paramName);
+            generateClosureExpr(sb, ternary.getTrueExpr(), paramName, 
beanMode);
             sb.append(") : (");
-            generateClosureExpr(sb, ternary.getFalseExpr(), paramName);
+            generateClosureExpr(sb, ternary.getFalseExpr(), paramName, 
beanMode);
             sb.append("))");
         } else if (expr instanceof MALExpressionModel.ClosureElvisExpr) {
             final MALExpressionModel.ClosureElvisExpr elvis =
                 (MALExpressionModel.ClosureElvisExpr) expr;
             sb.append("java.util.Optional.ofNullable(");
-            generateClosureExpr(sb, elvis.getPrimary(), paramName);
+            generateClosureExpr(sb, elvis.getPrimary(), paramName, beanMode);
             sb.append(").orElse(");
-            generateClosureExpr(sb, elvis.getFallback(), paramName);
+            generateClosureExpr(sb, elvis.getFallback(), paramName, beanMode);
             sb.append(")");
+        } else if (expr instanceof MALExpressionModel.ClosureRegexMatchExpr) {
+            final MALExpressionModel.ClosureRegexMatchExpr rm =
+                (MALExpressionModel.ClosureRegexMatchExpr) expr;
+            
sb.append(RUNTIME_HELPER_FQCN).append(".regexMatch(String.valueOf(");
+            generateClosureExpr(sb, rm.getTarget(), paramName, beanMode);
+            sb.append("), 
\"").append(escapeJava(rm.getPattern())).append("\")");
         }
     }
 
     private void generateClosureMethodChain(
             final StringBuilder sb,
             final MALExpressionModel.ClosureMethodChain chain,
-            final String paramName) {
-        // tags.key -> tags.get("key")
-        // tags['key'] -> tags.get("key")
+            final String paramName,
+            final boolean beanMode) {
         final String target = chain.getTarget();
         final String resolvedTarget = CLOSURE_CLASS_FQCN.getOrDefault(target, 
target);
         final boolean isClassRef = CLOSURE_CLASS_FQCN.containsKey(target);
@@ -867,7 +959,8 @@ public final class MALClassGenerator {
                         if (i > 0) {
                             local.append(", ");
                         }
-                        generateClosureExpr(local, mc.getArguments().get(i), 
paramName);
+                        generateClosureExpr(local, mc.getArguments().get(i), 
paramName,
+                            beanMode);
                     }
                     local.append(')');
                 } else if (seg instanceof 
MALExpressionModel.ClosureFieldAccess) {
@@ -880,11 +973,94 @@ public final class MALClassGenerator {
         }
 
         if (segs.isEmpty()) {
-            // Bare identifier (e.g. a local variable like "prefix", "result")
             sb.append(resolvedTarget);
             return;
         }
 
+        if (beanMode) {
+            // Bean mode: me.serviceName → me.getServiceName()
+            // me.layer.name() → me.getLayer().name()
+            // parts[0] → parts[0] (array index works as-is)
+            final StringBuilder local = new StringBuilder();
+            local.append(resolvedTarget);
+            for (final MALExpressionModel.ClosureChainSegment seg : segs) {
+                if (seg instanceof MALExpressionModel.ClosureFieldAccess) {
+                    final String name =
+                        ((MALExpressionModel.ClosureFieldAccess) 
seg).getName();
+                    if (target.equals(paramName) || 
local.toString().contains(".get")) {
+                        // Bean property on the closure parameter → getter
+                        local.append(".get")
+                          .append(Character.toUpperCase(name.charAt(0)))
+                          .append(name.substring(1)).append("()");
+                    } else {
+                        // Field access on a local variable (e.g., 
parts.length)
+                        local.append('.').append(name);
+                    }
+                } else if (seg instanceof 
MALExpressionModel.ClosureIndexAccess) {
+                    local.append('[');
+                    generateClosureExpr(local,
+                        ((MALExpressionModel.ClosureIndexAccess) 
seg).getIndex(), paramName,
+                        beanMode);
+                    local.append(']');
+                } else if (seg instanceof 
MALExpressionModel.ClosureMethodCallSeg) {
+                    final MALExpressionModel.ClosureMethodCallSeg mc =
+                        (MALExpressionModel.ClosureMethodCallSeg) seg;
+                    local.append('.').append(mc.getName()).append('(');
+                    for (int i = 0; i < mc.getArguments().size(); i++) {
+                        if (i > 0) {
+                            local.append(", ");
+                        }
+                        generateClosureExpr(local, mc.getArguments().get(i), 
paramName,
+                            beanMode);
+                    }
+                    local.append(')');
+                }
+            }
+            sb.append(local);
+            return;
+        }
+
+        // Local variable access (not closure param, not a class reference):
+        // e.g., matcher[0][1] → matcher[(int)0][(int)1]  (plain Java array 
access)
+        // e.g., parts.length → parts.length  (field access)
+        // e.g., parts.size() → parts.length  (Groovy .size() on arrays)
+        if (!target.equals(paramName) && !isClassRef) {
+            final StringBuilder local = new StringBuilder();
+            local.append(resolvedTarget);
+            for (final MALExpressionModel.ClosureChainSegment seg : segs) {
+                if (seg instanceof MALExpressionModel.ClosureIndexAccess) {
+                    local.append("[(int) ");
+                    generateClosureExpr(local,
+                        ((MALExpressionModel.ClosureIndexAccess) 
seg).getIndex(), paramName,
+                        beanMode);
+                    local.append(']');
+                } else if (seg instanceof 
MALExpressionModel.ClosureFieldAccess) {
+                    local.append('.').append(
+                        ((MALExpressionModel.ClosureFieldAccess) 
seg).getName());
+                } else if (seg instanceof 
MALExpressionModel.ClosureMethodCallSeg) {
+                    final MALExpressionModel.ClosureMethodCallSeg mc =
+                        (MALExpressionModel.ClosureMethodCallSeg) seg;
+                    // Groovy .size() on arrays → Java .length
+                    if ("size".equals(mc.getName()) && 
mc.getArguments().isEmpty()) {
+                        local.append(".length");
+                    } else {
+                        local.append('.').append(mc.getName()).append('(');
+                        for (int i = 0; i < mc.getArguments().size(); i++) {
+                            if (i > 0) {
+                                local.append(", ");
+                            }
+                            generateClosureExpr(local, 
mc.getArguments().get(i), paramName,
+                                beanMode);
+                        }
+                        local.append(')');
+                    }
+                }
+            }
+            sb.append(local);
+            return;
+        }
+
+        // Map mode (original): tags.key → tags.get("key")
         if (segs.size() == 1
                 && segs.get(0) instanceof 
MALExpressionModel.ClosureFieldAccess) {
             final String key =
@@ -895,7 +1071,8 @@ public final class MALClassGenerator {
                 && segs.get(0) instanceof 
MALExpressionModel.ClosureIndexAccess) {
             sb.append("(String) ").append(resolvedTarget).append(".get(");
             generateClosureExpr(sb,
-                ((MALExpressionModel.ClosureIndexAccess) 
segs.get(0)).getIndex(), paramName);
+                ((MALExpressionModel.ClosureIndexAccess) 
segs.get(0)).getIndex(), paramName,
+                beanMode);
             sb.append(")");
         } else {
             // General chain: build in a local buffer to support safe 
navigation
@@ -914,7 +1091,8 @@ public final class MALClassGenerator {
                     local.setLength(0);
                     local.append("(String) ").append(prior2).append(".get(");
                     generateClosureExpr(local,
-                        ((MALExpressionModel.ClosureIndexAccess) 
seg).getIndex(), paramName);
+                        ((MALExpressionModel.ClosureIndexAccess) 
seg).getIndex(), paramName,
+                        beanMode);
                     local.append(")");
                 } else if (seg instanceof 
MALExpressionModel.ClosureMethodCallSeg) {
                     final MALExpressionModel.ClosureMethodCallSeg mc =
@@ -929,7 +1107,8 @@ public final class MALClassGenerator {
                             if (i > 0) {
                                 local.append(", ");
                             }
-                            generateClosureExpr(local, 
mc.getArguments().get(i), paramName);
+                            generateClosureExpr(local, 
mc.getArguments().get(i), paramName,
+                                beanMode);
                         }
                         local.append("))");
                     } else {
@@ -938,7 +1117,8 @@ public final class MALClassGenerator {
                             if (i > 0) {
                                 local.append(", ");
                             }
-                            generateClosureExpr(local, 
mc.getArguments().get(i), paramName);
+                            generateClosureExpr(local, 
mc.getArguments().get(i), paramName,
+                                beanMode);
                         }
                         local.append(')');
                     }
@@ -951,48 +1131,55 @@ public final class MALClassGenerator {
     private void generateClosureCondition(final StringBuilder sb,
                                           final 
MALExpressionModel.ClosureCondition cond,
                                           final String paramName) {
+        generateClosureCondition(sb, cond, paramName, false);
+    }
+
+    private void generateClosureCondition(final StringBuilder sb,
+                                          final 
MALExpressionModel.ClosureCondition cond,
+                                          final String paramName,
+                                          final boolean beanMode) {
         if (cond instanceof MALExpressionModel.ClosureComparison) {
             final MALExpressionModel.ClosureComparison cc =
                 (MALExpressionModel.ClosureComparison) cond;
             switch (cc.getOp()) {
                 case EQ:
                     sb.append("java.util.Objects.equals(");
-                    generateClosureExpr(sb, cc.getLeft(), paramName);
+                    generateClosureExpr(sb, cc.getLeft(), paramName, beanMode);
                     sb.append(", ");
-                    generateClosureExpr(sb, cc.getRight(), paramName);
+                    generateClosureExpr(sb, cc.getRight(), paramName, 
beanMode);
                     sb.append(")");
                     break;
                 case NEQ:
                     sb.append("!java.util.Objects.equals(");
-                    generateClosureExpr(sb, cc.getLeft(), paramName);
+                    generateClosureExpr(sb, cc.getLeft(), paramName, beanMode);
                     sb.append(", ");
-                    generateClosureExpr(sb, cc.getRight(), paramName);
+                    generateClosureExpr(sb, cc.getRight(), paramName, 
beanMode);
                     sb.append(")");
                     break;
                 default:
-                    generateClosureExpr(sb, cc.getLeft(), paramName);
+                    generateClosureExpr(sb, cc.getLeft(), paramName, beanMode);
                     sb.append(comparisonOperator(cc.getOp()));
-                    generateClosureExpr(sb, cc.getRight(), paramName);
+                    generateClosureExpr(sb, cc.getRight(), paramName, 
beanMode);
                     break;
             }
         } else if (cond instanceof MALExpressionModel.ClosureLogical) {
             final MALExpressionModel.ClosureLogical lc =
                 (MALExpressionModel.ClosureLogical) cond;
             sb.append("(");
-            generateClosureCondition(sb, lc.getLeft(), paramName);
+            generateClosureCondition(sb, lc.getLeft(), paramName, beanMode);
             sb.append(lc.getOp() == MALExpressionModel.LogicalOp.AND ? " && " 
: " || ");
-            generateClosureCondition(sb, lc.getRight(), paramName);
+            generateClosureCondition(sb, lc.getRight(), paramName, beanMode);
             sb.append(")");
         } else if (cond instanceof MALExpressionModel.ClosureNot) {
             sb.append("!(");
             generateClosureCondition(sb,
-                ((MALExpressionModel.ClosureNot) cond).getInner(), paramName);
+                ((MALExpressionModel.ClosureNot) cond).getInner(), paramName, 
beanMode);
             sb.append(")");
         } else if (cond instanceof MALExpressionModel.ClosureExprCondition) {
-            // Truthiness check
             sb.append("(");
             generateClosureExpr(sb,
-                ((MALExpressionModel.ClosureExprCondition) cond).getExpr(), 
paramName);
+                ((MALExpressionModel.ClosureExprCondition) cond).getExpr(), 
paramName,
+                beanMode);
             sb.append(" != null)");
         } else if (cond instanceof MALExpressionModel.ClosureInCondition) {
             final MALExpressionModel.ClosureInCondition ic =
@@ -1005,7 +1192,7 @@ public final class MALClassGenerator {
                 
sb.append('"').append(escapeJava(ic.getValues().get(i))).append('"');
             }
             sb.append(").contains(");
-            generateClosureExpr(sb, ic.getExpr(), paramName);
+            generateClosureExpr(sb, ic.getExpr(), paramName, beanMode);
             sb.append(")");
         }
     }
@@ -1267,6 +1454,32 @@ public final class MALClassGenerator {
         return sb.toString();
     }
 
+    /**
+     * Whether the expression is a scalar (number-producing) function like 
{@code time()}.
+     */
+    private static boolean isScalarFunction(final MALExpressionModel.Expr 
expr) {
+        if (expr instanceof MALExpressionModel.FunctionCallExpr) {
+            final String fn = ((MALExpressionModel.FunctionCallExpr) 
expr).getFunctionName();
+            return "time".equals(fn);
+        }
+        return false;
+    }
+
+    /**
+     * Generate code for a scalar expression (literal number or scalar 
function).
+     */
+    private void generateScalarExpr(final StringBuilder sb,
+                                    final MALExpressionModel.Expr expr) {
+        if (expr instanceof MALExpressionModel.NumberExpr) {
+            sb.append(((MALExpressionModel.NumberExpr) expr).getValue());
+        } else if (isScalarFunction(expr)) {
+            final String fn = ((MALExpressionModel.FunctionCallExpr) 
expr).getFunctionName();
+            if ("time".equals(fn)) {
+                sb.append("(double) java.time.Instant.now().getEpochSecond()");
+            }
+        }
+    }
+
     private static String opMethodName(final MALExpressionModel.ArithmeticOp 
op) {
         switch (op) {
             case ADD:
@@ -1302,6 +1515,17 @@ public final class MALClassGenerator {
             || "SUM_PER_MIN".equals(name) || "MAX".equals(name) || 
"MIN".equals(name);
     }
 
+    /**
+     * Extracts a constant string key from a closure expression (used for bean 
setter naming).
+     * Returns the key string if the expression is a string literal, or null 
otherwise.
+     */
+    private static String extractConstantKey(final 
MALExpressionModel.ClosureExpr expr) {
+        if (expr instanceof MALExpressionModel.ClosureStringLiteral) {
+            return ((MALExpressionModel.ClosureStringLiteral) expr).getValue();
+        }
+        return null;
+    }
+
     private static String escapeJava(final String s) {
         return s.replace("\\", "\\\\")
                 .replace("\"", "\\\"")
diff --git 
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALExpressionModel.java
 
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALExpressionModel.java
index 86466a29ef..d32bacc2f4 100644
--- 
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALExpressionModel.java
+++ 
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALExpressionModel.java
@@ -453,6 +453,46 @@ public final class MALExpressionModel {
         }
     }
 
+    /**
+     * Ternary with explicit comparison condition: {@code left op right ? 
trueExpr : falseExpr}.
+     * E.g., {@code parts.length > 0 ? parts[0] : ''}.
+     */
+    @Getter
+    public static final class ClosureCompTernaryExpr implements ClosureExpr {
+        private final ClosureExpr left;
+        private final CompareOp op;
+        private final ClosureExpr right;
+        private final ClosureExpr trueExpr;
+        private final ClosureExpr falseExpr;
+
+        public ClosureCompTernaryExpr(final ClosureExpr left,
+                                      final CompareOp op,
+                                      final ClosureExpr right,
+                                      final ClosureExpr trueExpr,
+                                      final ClosureExpr falseExpr) {
+            this.left = left;
+            this.op = op;
+            this.right = right;
+            this.trueExpr = trueExpr;
+            this.falseExpr = falseExpr;
+        }
+    }
+
+    /**
+     * Groovy regex match: {@code expr =~ /pattern/}.
+     * Represents a regex match that produces a {@code 
java.util.regex.Matcher}.
+     */
+    @Getter
+    public static final class ClosureRegexMatchExpr implements ClosureExpr {
+        private final ClosureExpr target;
+        private final String pattern;
+
+        public ClosureRegexMatchExpr(final ClosureExpr target, final String 
pattern) {
+            this.target = target;
+            this.pattern = pattern;
+        }
+    }
+
     // ==================== Closure chain segments ====================
 
     public interface ClosureChainSegment {
diff --git 
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALScriptParser.java
 
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALScriptParser.java
index e7f8e2d336..d154e1f031 100644
--- 
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALScriptParser.java
+++ 
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALScriptParser.java
@@ -20,6 +20,8 @@ package org.apache.skywalking.oap.meter.analyzer.v2.compiler;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import org.antlr.v4.runtime.BaseErrorListener;
 import org.antlr.v4.runtime.CharStreams;
 import org.antlr.v4.runtime.CommonTokenStream;
@@ -81,8 +83,38 @@ public final class MALScriptParser {
     private MALScriptParser() {
     }
 
+    /**
+     * Pre-process expression to convert Groovy regex literals used as method
+     * arguments into string literals. E.g., {@code split(/\|/, -1)} becomes
+     * {@code split("\\|", -1)}. Regex literals after {@code =~} are handled
+     * by the lexer mode and are NOT touched here.
+     */
+    static String preprocessRegexLiterals(final String expression) {
+        // Match /pattern/ that appears after ( or , (method arg context),
+        // but NOT after =~ (which is handled by lexer mode)
+        final Pattern argRegex = Pattern.compile(
+            "(?<=[,(])\\s*/([^/\\r\\n]+)/");
+        final Matcher m = argRegex.matcher(expression);
+        if (!m.find()) {
+            return expression;
+        }
+        final StringBuffer sb = new StringBuffer();
+        m.reset();
+        while (m.find()) {
+            final String body = m.group(1);
+            // Preserve leading whitespace from the match
+            final String leading = m.group().substring(0, 
m.group().indexOf('/'));
+            m.appendReplacement(sb,
+                java.util.regex.Matcher.quoteReplacement(
+                    leading + "\"" + body + "\""));
+        }
+        m.appendTail(sb);
+        return sb.toString();
+    }
+
     public static Expr parse(final String expression) {
-        final MALLexer lexer = new 
MALLexer(CharStreams.fromString(expression));
+        final String preprocessed = preprocessRegexLiterals(expression);
+        final MALLexer lexer = new 
MALLexer(CharStreams.fromString(preprocessed));
         final CommonTokenStream tokens = new CommonTokenStream(lexer);
         final MALParser parser = new MALParser(tokens);
 
@@ -306,8 +338,24 @@ public final class MALScriptParser {
             }
             if (ctx.variableDeclaration() != null) {
                 final MALParser.VariableDeclarationContext vd = 
ctx.variableDeclaration();
+                final String typeName;
+                final String varName;
+                if (vd.DEF() != null) {
+                    // def keyword: def matcher = ...
+                    // Infer type from initializer
+                    varName = vd.IDENTIFIER(0).getText();
+                    final ClosureExpr init = 
convertClosureExpr(vd.closureExpr());
+                    typeName = inferDefType(init);
+                    return new ClosureVarDecl(typeName, varName, init);
+                }
+                if (vd.L_BRACKET() != null) {
+                    // Array type: String[] parts = ...
+                    typeName = vd.IDENTIFIER(0).getText() + "[]";
+                } else {
+                    typeName = vd.IDENTIFIER(0).getText();
+                }
                 return new ClosureVarDecl(
-                    vd.IDENTIFIER(0).getText(),
+                    typeName,
                     vd.IDENTIFIER(1).getText(),
                     convertClosureExpr(vd.closureExpr()));
             }
@@ -447,7 +495,64 @@ public final class MALScriptParser {
             return new 
ClosureExprCondition(convertClosureExpr(exprCtx.closureExpr()));
         }
 
+        /**
+         * Infer the Java type for a {@code def} variable declaration from its 
initializer.
+         * <ul>
+         *   <li>Regex match ({@code =~}) produces {@code String[][]}</li>
+         *   <li>Method chain ending in {@code .split()} produces {@code 
String[]}</li>
+         *   <li>Otherwise defaults to {@code Object}</li>
+         * </ul>
+         */
+        private String inferDefType(final ClosureExpr init) {
+            if (init instanceof MALExpressionModel.ClosureRegexMatchExpr) {
+                return "String[][]";
+            }
+            if (init instanceof ClosureMethodChain) {
+                final ClosureMethodChain chain = (ClosureMethodChain) init;
+                final List<MALExpressionModel.ClosureChainSegment> segs = 
chain.getSegments();
+                if (!segs.isEmpty()) {
+                    final MALExpressionModel.ClosureChainSegment last =
+                        segs.get(segs.size() - 1);
+                    if (last instanceof MALExpressionModel.ClosureMethodCallSeg
+                            && "split".equals(
+                                ((MALExpressionModel.ClosureMethodCallSeg) 
last).getName())) {
+                        return "String[]";
+                    }
+                }
+            }
+            return "Object";
+        }
+
+        private CompareOp convertCompOp(final MALParser.CompOpContext ctx) {
+            if (ctx.GT() != null) {
+                return CompareOp.GT;
+            }
+            if (ctx.LT() != null) {
+                return CompareOp.LT;
+            }
+            if (ctx.GTE() != null) {
+                return CompareOp.GTE;
+            }
+            if (ctx.LTE() != null) {
+                return CompareOp.LTE;
+            }
+            if (ctx.DEQ() != null) {
+                return CompareOp.EQ;
+            }
+            return CompareOp.NEQ;
+        }
+
         private ClosureExpr convertClosureExpr(final 
MALParser.ClosureExprContext ctx) {
+            if (ctx instanceof MALParser.ClosureTernaryCompContext) {
+                final MALParser.ClosureTernaryCompContext tc =
+                    (MALParser.ClosureTernaryCompContext) ctx;
+                return new MALExpressionModel.ClosureCompTernaryExpr(
+                    convertClosureExpr(tc.closureExpr(0)),
+                    convertCompOp(tc.compOp()),
+                    convertClosureExpr(tc.closureExpr(1)),
+                    convertClosureExpr(tc.closureExpr(2)),
+                    convertClosureExpr(tc.closureExpr(3)));
+            }
             if (ctx instanceof MALParser.ClosureTernaryContext) {
                 final MALParser.ClosureTernaryContext ternary =
                     (MALParser.ClosureTernaryContext) ctx;
@@ -456,6 +561,15 @@ public final class MALScriptParser {
                     convertClosureExpr(ternary.closureExpr(1)),
                     convertClosureExpr(ternary.closureExpr(2)));
             }
+            if (ctx instanceof MALParser.ClosureRegexMatchContext) {
+                final MALParser.ClosureRegexMatchContext rm =
+                    (MALParser.ClosureRegexMatchContext) ctx;
+                final String rawRegex = rm.REGEX_LITERAL().getText();
+                // Strip surrounding slashes: /pattern/ → pattern
+                final String pattern = rawRegex.substring(1, rawRegex.length() 
- 1);
+                return new MALExpressionModel.ClosureRegexMatchExpr(
+                    convertClosureExpr(rm.closureExpr()), pattern);
+            }
             if (ctx instanceof MALParser.ClosureElvisContext) {
                 final MALParser.ClosureElvisContext elvis =
                     (MALParser.ClosureElvisContext) ctx;
@@ -500,8 +614,9 @@ public final class MALScriptParser {
         private ClosureExpr convertClosureExprPrimary(
                 final MALParser.ClosureExprPrimaryContext ctx) {
             if (ctx instanceof MALParser.ClosureStringContext) {
-                return new ClosureStringLiteral(
-                    stripQuotes(((MALParser.ClosureStringContext) 
ctx).STRING().getText()));
+                final String raw =
+                    stripQuotes(((MALParser.ClosureStringContext) 
ctx).STRING().getText());
+                return expandGString(raw);
             }
             if (ctx instanceof MALParser.ClosureNumberContext) {
                 return new ClosureNumberLiteral(
@@ -583,6 +698,93 @@ public final class MALScriptParser {
         }
     }
 
+    /**
+     * Expand Groovy GString interpolation: {@code "text ${expr} more"} becomes
+     * a concatenation chain: {@code "text " + expr + " more"}.
+     * If the string contains no {@code ${...}} patterns, returns a plain
+     * {@link ClosureStringLiteral}.
+     */
+    static MALExpressionModel.ClosureExpr expandGString(final String raw) {
+        if (!raw.contains("${")) {
+            return new MALExpressionModel.ClosureStringLiteral(raw);
+        }
+
+        final List<MALExpressionModel.ClosureExpr> parts = new ArrayList<>();
+        int pos = 0;
+        while (pos < raw.length()) {
+            final int dollarBrace = raw.indexOf("${", pos);
+            if (dollarBrace < 0) {
+                // Remaining text
+                parts.add(new MALExpressionModel.ClosureStringLiteral(
+                    raw.substring(pos)));
+                break;
+            }
+            // Text before ${
+            if (dollarBrace > pos) {
+                parts.add(new MALExpressionModel.ClosureStringLiteral(
+                    raw.substring(pos, dollarBrace)));
+            }
+            // Find matching }
+            int braceDepth = 1;
+            int i = dollarBrace + 2;
+            while (i < raw.length() && braceDepth > 0) {
+                if (raw.charAt(i) == '{') {
+                    braceDepth++;
+                } else if (raw.charAt(i) == '}') {
+                    braceDepth--;
+                }
+                i++;
+            }
+            final String innerExpr = raw.substring(dollarBrace + 2, i - 1);
+            // Parse the inner expression as a mini closure expression
+            parts.add(parseGStringInterpolation(innerExpr));
+            pos = i;
+        }
+
+        // Build concatenation chain
+        MALExpressionModel.ClosureExpr result = parts.get(0);
+        for (int i = 1; i < parts.size(); i++) {
+            result = new MALExpressionModel.ClosureBinaryExpr(
+                result, MALExpressionModel.ArithmeticOp.ADD, parts.get(i));
+        }
+        return result;
+    }
+
+    /**
+     * Parse a GString interpolation expression like {@code tags.service_name}
+     * or {@code log.service} into a {@link 
MALExpressionModel.ClosureMethodChain}.
+     */
+    private static MALExpressionModel.ClosureExpr parseGStringInterpolation(
+            final String expr) {
+        // Simple dotted path: tags.service_name, me.serviceName, etc.
+        // Split on dots and build a chain
+        final String[] dotParts = expr.split("\\.");
+        if (dotParts.length == 1) {
+            // Bare variable reference
+            return new MALExpressionModel.ClosureMethodChain(
+                dotParts[0], Collections.emptyList());
+        }
+        // Build chain: first part is target, rest are field accesses
+        final List<MALExpressionModel.ClosureChainSegment> segments = new 
ArrayList<>();
+        for (int i = 1; i < dotParts.length; i++) {
+            // Check for method call: name()
+            if (dotParts[i].endsWith("()")) {
+                final String methodName = dotParts[i].substring(
+                    0, dotParts[i].length() - 2);
+                segments.add(new MALExpressionModel.ClosureMethodCallSeg(
+                    methodName, Collections.emptyList(), false));
+            } else if (dotParts[i].endsWith(")")) {
+                // Method with args not supported in GString — treat as field
+                segments.add(new MALExpressionModel.ClosureFieldAccess(
+                    dotParts[i], false));
+            } else {
+                segments.add(new MALExpressionModel.ClosureFieldAccess(
+                    dotParts[i], false));
+            }
+        }
+        return new MALExpressionModel.ClosureMethodChain(dotParts[0], 
segments);
+    }
+
     static String stripQuotes(final String s) {
         if (s.length() >= 2 && (s.charAt(0) == '\'' || s.charAt(0) == '"')) {
             return s.substring(1, s.length() - 1);
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 7d757329da..1e8dfc0d5f 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
@@ -17,6 +17,8 @@
 
 package org.apache.skywalking.oap.meter.analyzer.v2.compiler.rt;
 
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import org.apache.skywalking.oap.meter.analyzer.v2.dsl.Sample;
 import org.apache.skywalking.oap.meter.analyzer.v2.dsl.SampleFamily;
 import org.apache.skywalking.oap.meter.analyzer.v2.dsl.SampleFamilyBuilder;
@@ -31,6 +33,28 @@ public final class MalRuntimeHelper {
     private MalRuntimeHelper() {
     }
 
+    /**
+     * Groovy regex match ({@code =~}): returns a {@code String[][]} where 
each row is
+     * one match with group 0 (full match) and capture groups 1..N.
+     * Returns {@code null} if the pattern does not match, so that Groovy-style
+     * truthiness checks ({@code matcher ? matcher[0][1] : "unknown"}) work 
via null check.
+     */
+    public static String[][] regexMatch(final String input, final String 
regex) {
+        if (input == null) {
+            return null;
+        }
+        final Matcher m = Pattern.compile(regex).matcher(input);
+        if (!m.find()) {
+            return null;
+        }
+        final int groupCount = m.groupCount();
+        final String[] row = new String[groupCount + 1];
+        for (int i = 0; i <= groupCount; i++) {
+            row[i] = m.group(i);
+        }
+        return new String[][] {row};
+    }
+
     /**
      * Reverse division: computes {@code numerator / v} for each sample value 
{@code v}.
      * Used by generated code for {@code Number / SampleFamily} expressions.
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 c2d2b403bf..66835f2f53 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
@@ -358,4 +358,45 @@ class MALClassGeneratorTest {
         assertNotNull(expr);
         assertNotNull(expr.run(java.util.Map.of()));
     }
+
+    @Test
+    void regexMatchWithDefCompiles() throws Exception {
+        // envoy-ca pattern: def + regex match + ternary with chained indexing
+        final MalExpression expr = generator.compile(
+            "test_regex",
+            "metric.tag({tags ->\n"
+            + "  def matcher = (tags.metrics_name =~ 
/\\.ssl\\.certificate\\.([^.]+)\\.expiration/)\n"
+            + "  tags.secret_name = matcher ? matcher[0][1] : \"unknown\"\n"
+            + "})");
+        assertNotNull(expr);
+        assertNotNull(expr.run(java.util.Map.of()));
+    }
+
+    @Test
+    void envoyCAExpressionCompiles() throws Exception {
+        // Full envoy-ca.yaml expression with regex closure, subtraction of 
time(), and service
+        final MalExpression expr = generator.compile(
+            "test_envoy_ca",
+            "(metric.tagMatch('metrics_name', 
'.*ssl.*expiration_unix_time_seconds')"
+            + ".tag({tags ->\n"
+            + "  def matcher = (tags.metrics_name =~ 
/\\.ssl\\.certificate\\.([^.]+)"
+            + "\\.expiration_unix_time_seconds/)\n"
+            + "  tags.secret_name = matcher ? matcher[0][1] : \"unknown\"\n"
+            + "}).min(['app', 'secret_name']) - time())"
+            + ".downsampling(MIN).service(['app'], Layer.MESH_DP)");
+        assertNotNull(expr);
+    }
+
+    @Test
+    void timeScalarFunctionHandledInMetadata() throws Exception {
+        // time() should not appear as a sample name and should be treated as 
scalar
+        final MalExpression expr = generator.compile(
+            "test_time",
+            "(metric.sum(['app']) - time()).service(['app'], Layer.GENERAL)");
+        assertNotNull(expr);
+        assertNotNull(expr.metadata());
+        // time() should not be in sample names
+        assertTrue(expr.metadata().getSamples().contains("metric"));
+        assertTrue(expr.metadata().getSamples().size() == 1);
+    }
 }
diff --git 
a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALScriptParserTest.java
 
b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALScriptParserTest.java
index 01d3209628..e203c364be 100644
--- 
a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALScriptParserTest.java
+++ 
b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALScriptParserTest.java
@@ -351,6 +351,53 @@ class MALScriptParserTest {
             chain.getSegments().get(0));
     }
 
+    @Test
+    void parseDefWithRegexMatch() {
+        // def matcher = (tags.metrics_name =~ /pattern/)
+        final MALExpressionModel.Expr ast = MALScriptParser.parse(
+            "metric.tag({tags ->\n"
+            + "  def matcher = (tags.metrics_name =~ /\\.ssl\\.([^.]+)/)\n"
+            + "  tags.secret_name = matcher ? matcher[0][1] : \"unknown\"\n"
+            + "})");
+        assertInstanceOf(MetricExpr.class, ast);
+        final MetricExpr metric = (MetricExpr) ast;
+        final ClosureArgument closure =
+            (ClosureArgument) 
metric.getMethodChain().get(0).getArguments().get(0);
+        assertEquals(2, closure.getBody().size());
+
+        // First statement: def variable declaration
+        assertInstanceOf(MALExpressionModel.ClosureVarDecl.class, 
closure.getBody().get(0));
+        final MALExpressionModel.ClosureVarDecl vd =
+            (MALExpressionModel.ClosureVarDecl) closure.getBody().get(0);
+        assertEquals("String[][]", vd.getTypeName());
+        assertEquals("matcher", vd.getVarName());
+        // Initializer should be a regex match expression
+        assertInstanceOf(MALExpressionModel.ClosureRegexMatchExpr.class, 
vd.getInitializer());
+        final MALExpressionModel.ClosureRegexMatchExpr rm =
+            (MALExpressionModel.ClosureRegexMatchExpr) vd.getInitializer();
+        assertEquals("\\.ssl\\.([^.]+)", rm.getPattern());
+
+        // Second statement: ternary with chained indexing
+        assertInstanceOf(MALExpressionModel.ClosureAssignment.class, 
closure.getBody().get(1));
+    }
+
+    @Test
+    void parseTimeFunctionCall() {
+        // (expr - time()).downsampling(MIN)
+        final MALExpressionModel.Expr ast = MALScriptParser.parse(
+            "(metric.min(['app']) - time()).downsampling(MIN).service(['app'], 
Layer.MESH_DP)");
+        assertInstanceOf(ParenChainExpr.class, ast);
+        final ParenChainExpr pce = (ParenChainExpr) ast;
+        assertInstanceOf(BinaryExpr.class, pce.getInner());
+        final BinaryExpr bin = (BinaryExpr) pce.getInner();
+        assertEquals(MALExpressionModel.ArithmeticOp.SUB, bin.getOp());
+        assertInstanceOf(MALExpressionModel.FunctionCallExpr.class, 
bin.getRight());
+        final MALExpressionModel.FunctionCallExpr timeFn =
+            (MALExpressionModel.FunctionCallExpr) bin.getRight();
+        assertEquals("time", timeFn.getFunctionName());
+        assertEquals(0, timeFn.getArguments().size());
+    }
+
     @Test
     void parseSyntaxErrorThrows() {
         assertThrows(IllegalArgumentException.class,
diff --git 
a/test/script-cases/scripts/mal/test-envoy-metrics-rules/envoy-ca.yaml 
b/test/script-cases/scripts/mal/test-envoy-metrics-rules/envoy-ca.yaml
new file mode 100644
index 0000000000..570dec64df
--- /dev/null
+++ b/test/script-cases/scripts/mal/test-envoy-metrics-rules/envoy-ca.yaml
@@ -0,0 +1,60 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This will parse a textual representation of a duration. The formats
+# accepted are based on the ISO-8601 duration format {@code PnDTnHnMn.nS}
+# with days considered to be exactly 24 hours.
+# <p>
+# Examples:
+# <pre>
+#    "PT20.345S" -- parses as "20.345 seconds"
+#    "PT15M"     -- parses as "15 minutes" (where a minute is 60 seconds)
+#    "PT10H"     -- parses as "10 hours" (where an hour is 3600 seconds)
+#    "P2D"       -- parses as "2 days" (where a day is 24 hours or 86400 
seconds)
+#    "P2DT3H4M"  -- parses as "2 days, 3 hours and 4 minutes"
+#    "P-6H3M"    -- parses as "-6 hours and +3 minutes"
+#    "-P6H3M"    -- parses as "-6 hours and -3 minutes"
+#    "-P-6H+3M"  -- parses as "+6 hours and -3 minutes"
+# </pre>
+
+metricPrefix: envoy
+metricsRules:
+  - name: service_cluster_ssl_ca_expiration_seconds
+    exp: |-
+      (envoy_cluster_metrics.tagMatch('metrics_name' , 
'.*ssl.*expiration_unix_time_seconds').tag({ tags ->
+        def matcher = (tags.metrics_name =~ 
/\.ssl.certificate\.([^.]+)\.expiration_unix_time_seconds/)
+        tags.secret_name = matcher ? matcher[0][1] : "unknown"
+      }).min(['app', 'secret_name']) - 
time()).downsampling(MIN).service(['app'], Layer.MESH_DP)
+
+  - name: service_listener_ssl_ca_expiration_seconds
+    exp: |-
+      (envoy_listener_metrics.tagMatch('metrics_name' , 
'.*ssl.*expiration_unix_time_seconds').tag({ tags ->
+        def matcher = (tags.metrics_name =~ 
/\.ssl.certificate\.([^.]+)\.expiration_unix_time_seconds/)
+        tags.secret_name = matcher ? matcher[0][1] : "unknown"
+      }).min(['app', 'secret_name']) - 
time()).downsampling(MIN).service(['app'], Layer.MESH_DP)
+
+  - name: instance_cluster_ssl_ca_expiration_seconds
+    exp: |-
+      (envoy_cluster_metrics.tagMatch('metrics_name' , 
'.*ssl.*expiration_unix_time_seconds').tag({ tags ->
+        def matcher = (tags.metrics_name =~ 
/\.ssl.certificate\.([^.]+)\.expiration_unix_time_seconds/)
+        tags.secret_name = matcher ? matcher[0][1] : "unknown"
+      }).min(['app', 'instance', 'secret_name']) - 
time()).downsampling(MIN).instance(['app'], ['instance'], Layer.MESH_DP)
+
+  - name: instance_listener_ssl_ca_expiration_seconds
+    exp: |-
+      (envoy_listener_metrics.tagMatch('metrics_name' , 
'.*ssl.*expiration_unix_time_seconds').tag({ tags ->
+        def matcher = (tags.metrics_name =~ 
/\.ssl.certificate\.([^.]+)\.expiration_unix_time_seconds/)
+        tags.secret_name = matcher ? matcher[0][1] : "unknown"
+      }).min(['app', 'instance', 'secret_name']) - 
time()).downsampling(MIN).instance(['app'], ['instance'], Layer.MESH_DP)
diff --git 
a/test/script-cases/scripts/mal/test-meter-analyzer-config/satellite-tag-prefix.yaml
 
b/test/script-cases/scripts/mal/test-meter-analyzer-config/satellite-tag-prefix.yaml
new file mode 100644
index 0000000000..46e0c29765
--- /dev/null
+++ 
b/test/script-cases/scripts/mal/test-meter-analyzer-config/satellite-tag-prefix.yaml
@@ -0,0 +1,36 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Variant of satellite.yaml that prepends "satellite::" to the service name
+# via a tag() closure in expSuffix before calling service().
+expSuffix: tag({tags -> tags.service = 'satellite::' + 
tags.service}).service(['service'], Layer.SO11Y_SATELLITE)
+metricPrefix: satellite
+metricsRules:
+  - name: service_receive_event_count
+    exp: sw_stl_gatherer_receive_count.sum(["pipe", "status", 
"service"]).increase("PT1M")
+  - name: service_fetch_event_count
+    exp: sw_stl_gatherer_fetch_count.sum(["pipe", "status", 
"service"]).increase("PT1M")
+  - name: service_queue_input_count
+    exp: sw_stl_queue_output_count.sum(["pipe", "status", 
"service"]).increase("PT1M")
+  - name: service_send_event_count
+    exp: sw_stl_sender_output_count.sum(["pipe", "status", 
"service"]).increase("PT1M")
+  - name: service_queue_total_capacity
+    exp: sw_stl_pipeline_queue_total_capacity.sum(["pipeline", "service"])
+  - name: service_queue_used_count
+    exp: sw_stl_pipeline_queue_partition_size.sum(["pipeline", "service"])
+  - name: service_server_cpu_utilization
+    exp: sw_stl_grpc_server_cpu_gauge
+  - name: service_grpc_connect_count
+    exp: sw_stl_grpc_server_connection_count
diff --git 
a/test/script-cases/scripts/mal/test-otel-rules/service-decorate-attributes.yaml
 
b/test/script-cases/scripts/mal/test-otel-rules/service-decorate-attributes.yaml
new file mode 100644
index 0000000000..bb75950109
--- /dev/null
+++ 
b/test/script-cases/scripts/mal/test-otel-rules/service-decorate-attributes.yaml
@@ -0,0 +1,40 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Test case: service-level decorate() with attr0-attr5.
+# Validates decorate closure operating on MeterEntity bean properties.
+# The service name is a pipe-delimited composite: 
"-|svcName|namespace|cluster|-"
+# decorate() splits it and stores each part in attr0-attr5.
+
+#  Metric Values
+#    0 = Not Satisfied
+#    1 = Satisfied
+#    2 = Not Evaluated
+filter: "{ tags -> tags.job_name == 'control-monitor' }"
+expPrefix: tag({ tags -> tags.service = 
"-|${tags.service_name}|${tags.service_namespace}|${tags.cluster_name}|-".toString()
 })
+expSuffix: |-
+  service(['service'], Layer.MESH).decorate({ me ->
+    me.attr0 = 'service'
+    String[] parts = (me.serviceName ?: '').split("\\|", -1)
+    me.attr1 = parts.length > 0 ? parts[0] : ''
+    me.attr2 = parts.length > 1 ? parts[1] : ''
+    me.attr3 = parts.length > 2 ? parts[2] : ''
+    me.attr4 = parts.length > 3 ? parts[3] : ''
+    me.attr5 = parts.length > 4 ? parts[4] : ''
+  })
+metricPrefix: meter_control
+metricsRules:
+  - name: last_status_by_service
+    exp: control_last_status_by_service.tagNotEqual('service_name' , 
null).sum(['service','control_bundle','control_name']).downsampling(LATEST)
diff --git 
a/test/script-cases/scripts/mal/test-otel-rules/service-gstring-regex-split.yaml
 
b/test/script-cases/scripts/mal/test-otel-rules/service-gstring-regex-split.yaml
new file mode 100644
index 0000000000..da18d551d7
--- /dev/null
+++ 
b/test/script-cases/scripts/mal/test-otel-rules/service-gstring-regex-split.yaml
@@ -0,0 +1,20 @@
+#  Metric Values
+#    0 = Not Satisfied
+#    1 = Satisfied
+#    2 = Not Evaluated
+filter: "{ tags -> tags.job_name == 'oscal-control' }" # The OpenTelemetry job 
name
+expPrefix: tag({ tags -> tags.service = 
"-|${tags.service_name}|${tags.service_namespace}|${tags.cluster_name}|-".toString()
 })
+expSuffix: |-
+  service(['service'], Layer.MESH).decorate({ me ->
+    me.attr0 = 'service'
+    def parts = (me.serviceName ?: '').split(/\|/, -1)
+    me.attr1 = parts.size() > 0 ? parts[0] : ''
+    me.attr2 = parts.size() > 1 ? parts[1] : ''
+    me.attr3 = parts.size() > 2 ? parts[2] : ''
+    me.attr4 = parts.size() > 3 ? parts[3] : ''
+    me.attr5 = parts.size() > 4 ? parts[4] : ''
+  })
+metricPrefix: meter_oscal_control
+metricsRules:
+  - name: last_status_by_service
+    exp: oscal_control_last_status_by_service.tagNotEqual('service_name' , 
null).sum(['service','oscal_control_bundle','oscal_control_name']).downsampling(LATEST)

Reply via email to