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)
