You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2026/02/21 11:58:27 UTC
(skywalking-graalvm-distro) 01/02: MAL-to-Java transpiler: compile Groovy DSL expressions to pure Java at build time
This is an automated email from the ASF dual-hosted git repository.
wusheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-graalvm-distro.git
commit e26cc15d238466ce44f95e586151cccc721620a2
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Sat Feb 21 19:38:33 2026 +0800
MAL-to-Java transpiler: compile Groovy DSL expressions to pure Java at build time
Transpile all 1250 MAL expressions and 29 filter closures from Groovy DSL
to pure Java classes implementing MalExpression/MalFilter interfaces. This
eliminates Groovy runtime dependency for MAL evaluation in GraalVM native image.
Precompiler changes:
- MalToJavaTranspiler: Groovy AST → Java source transpiler (67 unit tests)
- DSL.java: added EXPRESSION_REGISTRY to capture expression strings
- Precompiler.transpileMAL(): wired into build pipeline after Groovy compilation
- Same-FQCN stubs (MalExpression, MalFilter, SampleFamilyFunctions) for javac
meter-analyzer-for-graalvm changes:
- ExpressionParsingContext: same-FQCN with public get()/create() for cross-package access
- SampleFamily: tagNotEqual/tagMatch/tagNotMatch changed to varargs, newValue made public
- FilterExpression: wrapped Closure in SampleFilter lambda for GraalVM SampleFamily API
- MalExpression, MalFilter, SampleFamilyFunctions: Java functional interface definitions
- pom.xml: added ExpressionParsingContext to shade excludes
---
.../skywalking/oap/meter/analyzer/dsl/DSL.java | 8 +
.../oap/meter/analyzer/dsl/MalExpression.java | 43 +
.../oap/meter/analyzer/dsl/MalFilter.java | 42 +
.../meter/analyzer/dsl/SampleFamilyFunctions.java | 78 ++
.../precompiler/MalToJavaTranspiler.java | 1232 ++++++++++++++++++++
.../server/buildtools/precompiler/Precompiler.java | 325 ++++++
.../precompiler/MalToJavaTranspilerTest.java | 982 ++++++++++++++++
.../meter-analyzer-for-graalvm/pom.xml | 5 +
.../analyzer/dsl/ExpressionParsingContext.java | 100 ++
.../oap/meter/analyzer/dsl/FilterExpression.java | 2 +-
.../oap/meter/analyzer/dsl/MalExpression.java | 40 +
.../oap/meter/analyzer/dsl/MalFilter.java | 39 +
.../oap/meter/analyzer/dsl/SampleFamily.java | 913 +++++++++++++++
.../meter/analyzer/dsl/SampleFamilyFunctions.java | 75 ++
14 files changed, 3883 insertions(+), 1 deletion(-)
diff --git a/build-tools/precompiler/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/DSL.java b/build-tools/precompiler/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/DSL.java
index f6e622e..930112f 100644
--- a/build-tools/precompiler/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/DSL.java
+++ b/build-tools/precompiler/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/DSL.java
@@ -56,6 +56,7 @@ public final class DSL {
private static final Map<String, String> SCRIPT_REGISTRY = new LinkedHashMap<>();
private static final Map<String, Integer> METRIC_OCCURRENCE = new HashMap<>();
private static final Map<String, String> EXPRESSION_HASHES = new LinkedHashMap<>();
+ private static final Map<String, String> EXPRESSION_REGISTRY = new LinkedHashMap<>();
public static void setTargetDirectory(File dir) {
TARGET_DIRECTORY = dir;
@@ -69,11 +70,17 @@ public final class DSL {
return Collections.unmodifiableMap(EXPRESSION_HASHES);
}
+ /** Returns scriptName → expression string mapping for transpiler. */
+ public static Map<String, String> getExpressionRegistry() {
+ return Collections.unmodifiableMap(EXPRESSION_REGISTRY);
+ }
+
public static void reset() {
TARGET_DIRECTORY = null;
SCRIPT_REGISTRY.clear();
METRIC_OCCURRENCE.clear();
EXPRESSION_HASHES.clear();
+ EXPRESSION_REGISTRY.clear();
}
/**
@@ -137,6 +144,7 @@ public final class DSL {
if (metricName != null && TARGET_DIRECTORY != null) {
SCRIPT_REGISTRY.put(scriptName, script.getClass().getName());
EXPRESSION_HASHES.put(scriptName, sha256(expression));
+ EXPRESSION_REGISTRY.put(scriptName, expression);
log.debug("Compiled MAL script: {} -> {}", scriptName, script.getClass().getName());
}
diff --git a/build-tools/precompiler/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/MalExpression.java b/build-tools/precompiler/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/MalExpression.java
new file mode 100644
index 0000000..281318b
--- /dev/null
+++ b/build-tools/precompiler/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/MalExpression.java
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+package org.apache.skywalking.oap.meter.analyzer.dsl;
+
+import java.util.Map;
+
+/**
+ * Interface for generated MAL (Meter Analysis Language) expression classes.
+ * Each MAL expression from rule YAML files is transpiled to a pure Java class
+ * implementing this interface at build time.
+ *
+ * Replaces the Groovy DelegatingScript approach — no Groovy runtime needed.
+ *
+ * Same-FQCN copy for precompiler build-time classpath.
+ * Canonical version: meter-analyzer-for-graalvm.
+ */
+@FunctionalInterface
+public interface MalExpression {
+
+ /**
+ * Execute the MAL expression against the given sample families.
+ *
+ * @param samples map of metric name → SampleFamily, replaces Groovy's
+ * propertyMissing() delegate lookup
+ * @return the resulting SampleFamily, or SampleFamily.EMPTY if no data
+ */
+ SampleFamily run(Map<String, SampleFamily> samples);
+}
diff --git a/build-tools/precompiler/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/MalFilter.java b/build-tools/precompiler/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/MalFilter.java
new file mode 100644
index 0000000..bfcde47
--- /dev/null
+++ b/build-tools/precompiler/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/MalFilter.java
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+package org.apache.skywalking.oap.meter.analyzer.dsl;
+
+import java.util.Map;
+
+/**
+ * Interface for generated MAL filter expression classes.
+ * Each filter expression from rule YAML files is transpiled to a pure Java class
+ * implementing this interface at build time.
+ *
+ * Replaces the Groovy Closure<Boolean> approach — no Groovy runtime needed.
+ *
+ * Same-FQCN copy for precompiler build-time classpath.
+ * Canonical version: meter-analyzer-for-graalvm.
+ */
+@FunctionalInterface
+public interface MalFilter {
+
+ /**
+ * Test whether samples with the given tags should be included.
+ *
+ * @param tags the sample labels/tags to evaluate
+ * @return true if the sample should be kept, false to filter it out
+ */
+ boolean test(Map<String, String> tags);
+}
diff --git a/build-tools/precompiler/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/SampleFamilyFunctions.java b/build-tools/precompiler/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/SampleFamilyFunctions.java
new file mode 100644
index 0000000..805bd54
--- /dev/null
+++ b/build-tools/precompiler/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/SampleFamilyFunctions.java
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+
+package org.apache.skywalking.oap.meter.analyzer.dsl;
+
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import org.apache.skywalking.oap.server.core.analysis.meter.MeterEntity;
+
+/**
+ * Java functional interfaces replacing Groovy Closure parameters in SampleFamily methods.
+ * These are used by the generated MAL expression classes (transpiled from Groovy DSL).
+ *
+ * Same-FQCN copy for precompiler build-time classpath.
+ * Canonical version: meter-analyzer-for-graalvm.
+ */
+public final class SampleFamilyFunctions {
+
+ private SampleFamilyFunctions() {
+ }
+
+ /**
+ * Tag transformation function: receives mutable tag map, modifies it, returns modified map.
+ * Replaces Closure<?> in SampleFamily.tag().
+ */
+ @FunctionalInterface
+ public interface TagFunction extends Function<Map<String, String>, Map<String, String>> {
+ }
+
+ /**
+ * Sample filter predicate: receives tag map, returns true to keep the sample.
+ * Replaces Closure<Boolean> in SampleFamily.filter().
+ */
+ @FunctionalInterface
+ public interface SampleFilter extends Predicate<Map<String, String>> {
+ }
+
+ /**
+ * ForEach function: receives an array element and mutable label map.
+ * Replaces Closure<Void> in SampleFamily.forEach().
+ */
+ @FunctionalInterface
+ public interface ForEachFunction extends BiConsumer<String, Map<String, String>> {
+ }
+
+ /**
+ * Decorate function: receives a MeterEntity to modify.
+ * Replaces Closure<Void> in SampleFamily.decorate().
+ */
+ @FunctionalInterface
+ public interface DecorateFunction extends Consumer<MeterEntity> {
+ }
+
+ /**
+ * Properties extractor: receives label map, returns properties map.
+ * Replaces Closure<Map> in SampleFamily.instance() overload.
+ */
+ @FunctionalInterface
+ public interface PropertiesExtractor extends Function<Map<String, String>, Map<String, String>> {
+ }
+}
diff --git a/build-tools/precompiler/src/main/java/org/apache/skywalking/oap/server/buildtools/precompiler/MalToJavaTranspiler.java b/build-tools/precompiler/src/main/java/org/apache/skywalking/oap/server/buildtools/precompiler/MalToJavaTranspiler.java
new file mode 100644
index 0000000..cc7bfb5
--- /dev/null
+++ b/build-tools/precompiler/src/main/java/org/apache/skywalking/oap/server/buildtools/precompiler/MalToJavaTranspiler.java
@@ -0,0 +1,1232 @@
+/*
+ * 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.
+ */
+
+package org.apache.skywalking.oap.server.buildtools.precompiler;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.ToolProvider;
+import lombok.extern.slf4j.Slf4j;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.ModuleNode;
+import org.codehaus.groovy.ast.Parameter;
+import org.codehaus.groovy.ast.expr.ArgumentListExpression;
+import org.codehaus.groovy.ast.expr.BinaryExpression;
+import org.codehaus.groovy.ast.expr.ClosureExpression;
+import org.codehaus.groovy.ast.expr.ClassExpression;
+import org.codehaus.groovy.ast.expr.ConstantExpression;
+import org.codehaus.groovy.ast.expr.DeclarationExpression;
+import org.codehaus.groovy.ast.expr.ElvisOperatorExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.ListExpression;
+import org.codehaus.groovy.ast.expr.MapEntryExpression;
+import org.codehaus.groovy.ast.expr.MapExpression;
+import org.codehaus.groovy.ast.expr.MethodCallExpression;
+import org.codehaus.groovy.ast.expr.PropertyExpression;
+import org.codehaus.groovy.ast.expr.TernaryExpression;
+import org.codehaus.groovy.ast.expr.TupleExpression;
+import org.codehaus.groovy.ast.expr.VariableExpression;
+import org.codehaus.groovy.syntax.Types;
+import org.codehaus.groovy.ast.expr.BooleanExpression;
+import org.codehaus.groovy.ast.expr.NotExpression;
+import org.codehaus.groovy.ast.stmt.BlockStatement;
+import org.codehaus.groovy.ast.stmt.EmptyStatement;
+import org.codehaus.groovy.ast.stmt.ExpressionStatement;
+import org.codehaus.groovy.ast.stmt.IfStatement;
+import org.codehaus.groovy.ast.stmt.ReturnStatement;
+import org.codehaus.groovy.ast.stmt.Statement;
+import org.codehaus.groovy.control.CompilationUnit;
+import org.codehaus.groovy.control.CompilerConfiguration;
+import org.codehaus.groovy.control.Phases;
+
+/**
+ * Transpiles Groovy MAL expressions to Java source code at build time.
+ * Parses expression strings into Groovy AST (via CompilationUnit at CONVERSION phase),
+ * walks AST nodes, and produces equivalent Java classes implementing MalExpression or MalFilter.
+ *
+ * <p>Supported AST patterns:
+ * <ul>
+ * <li>Variable references: sample family lookups, DownsamplingType constants, KNOWN_TYPES</li>
+ * <li>Method chains: .sum(), .service(), .tagEqual(), .rate(), .histogram(), etc.</li>
+ * <li>Binary arithmetic with operand-swap logic per upstream ExpandoMetaClass
+ * (N-SF: sf.minus(N).negative(), N/SF: sf.newValue(v->N/v))</li>
+ * <li>tag() closures: TagFunction lambda (assignment, remove, string concat, if/else)</li>
+ * <li>Filter closures: MalFilter class (==, !=, in, truthiness, negation, &&, ||)</li>
+ * <li>forEach() closures: ForEachFunction lambda (var decls, if/else-if, early return)</li>
+ * <li>instance() with PropertiesExtractor closure: Map.of() from map literals</li>
+ * <li>Elvis (?:), safe navigation (?.), ternary (? :)</li>
+ * <li>Batch compilation via javax.tools.JavaCompiler + manifest writing</li>
+ * </ul>
+ *
+ * <p>See MAL-TRANSPILER-DESIGN.md for the full AST-to-Java mapping reference.
+ */
+@Slf4j
+public class MalToJavaTranspiler {
+
+ static final String GENERATED_PACKAGE =
+ "org.apache.skywalking.oap.server.core.source.oal.rt.mal";
+
+ private static final Set<String> DOWNSAMPLING_CONSTANTS = Set.of(
+ "AVG", "SUM", "LATEST", "SUM_PER_MIN", "MAX", "MIN"
+ );
+
+ /** Known enum/class names that appear as property bases (e.g. Layer.GENERAL). */
+ private static final Set<String> KNOWN_TYPES = Set.of(
+ "Layer", "DetectPoint", "K8sRetagType", "ProcessRegistry", "TimeUnit"
+ );
+
+ // ---- Batch state tracking ----
+
+ /** Generated expression sources: className → Java source code. */
+ private final Map<String, String> expressionSources = new LinkedHashMap<>();
+
+ /** Generated filter sources: className → Java source code. */
+ private final Map<String, String> filterSources = new LinkedHashMap<>();
+
+ /** Filter literal to class name mapping for manifest. */
+ private final Map<String, String> filterLiteralToClass = new LinkedHashMap<>();
+
+ /**
+ * Transpile a MAL expression to a Java class source implementing MalExpression.
+ *
+ * @param className simple class name (e.g. "MalExpr_meter_jvm_heap")
+ * @param expression the Groovy expression string
+ * @return generated Java source code
+ */
+ public String transpileExpression(String className, String expression) {
+ ModuleNode ast = parseToAST(expression);
+ Statement body = extractBody(ast);
+
+ Set<String> sampleNames = new LinkedHashSet<>();
+ collectSampleNames(body, sampleNames);
+
+ String javaBody = visitStatement(body);
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("package ").append(GENERATED_PACKAGE).append(";\n\n");
+ sb.append("import java.util.*;\n");
+ sb.append("import org.apache.skywalking.oap.meter.analyzer.dsl.*;\n");
+ sb.append("import org.apache.skywalking.oap.meter.analyzer.dsl.SampleFamilyFunctions.*;\n");
+ sb.append("import org.apache.skywalking.oap.server.core.analysis.Layer;\n");
+ sb.append("import org.apache.skywalking.oap.server.core.source.DetectPoint;\n");
+ sb.append("import org.apache.skywalking.oap.meter.analyzer.dsl.tagOpt.K8sRetagType;\n");
+ sb.append("import org.apache.skywalking.oap.meter.analyzer.dsl.registry.ProcessRegistry;\n\n");
+
+ sb.append("public class ").append(className).append(" implements MalExpression {\n");
+ sb.append(" @Override\n");
+ sb.append(" public SampleFamily run(Map<String, SampleFamily> samples) {\n");
+
+ if (!sampleNames.isEmpty()) {
+ sb.append(" ExpressionParsingContext.get().ifPresent(ctx -> {\n");
+ for (String name : sampleNames) {
+ sb.append(" ctx.getSamples().add(\"").append(escapeJava(name)).append("\");\n");
+ }
+ sb.append(" });\n");
+ }
+
+ sb.append(" return ").append(javaBody).append(";\n");
+ sb.append(" }\n");
+ sb.append("}\n");
+
+ return sb.toString();
+ }
+
+ /**
+ * Transpile a MAL filter literal to a Java class source implementing MalFilter.
+ * Filter literals are closures like: { tags -> tags.job_name == 'vm-monitoring' }
+ *
+ * @param className simple class name (e.g. "MalFilter_0")
+ * @param filterLiteral the Groovy closure literal string
+ * @return generated Java source code
+ */
+ public String transpileFilter(String className, String filterLiteral) {
+ ModuleNode ast = parseToAST(filterLiteral);
+ Statement body = extractBody(ast);
+
+ // The filter literal is a closure expression at the top level
+ ClosureExpression closure = extractClosure(body);
+ Parameter[] params = closure.getParameters();
+ String tagsVar = (params != null && params.length > 0) ? params[0].getName() : "tags";
+
+ // Get the body expression — may need to unwrap inner block/closure
+ Expression bodyExpr = extractFilterBodyExpr(closure.getCode(), tagsVar);
+
+ // Generate the boolean condition
+ String condition = visitFilterCondition(bodyExpr, tagsVar);
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("package ").append(GENERATED_PACKAGE).append(";\n\n");
+ sb.append("import java.util.*;\n");
+ sb.append("import org.apache.skywalking.oap.meter.analyzer.dsl.*;\n\n");
+
+ sb.append("public class ").append(className).append(" implements MalFilter {\n");
+ sb.append(" @Override\n");
+ sb.append(" public boolean test(Map<String, String> tags) {\n");
+ sb.append(" return ").append(condition).append(";\n");
+ sb.append(" }\n");
+ sb.append("}\n");
+
+ return sb.toString();
+ }
+
+ /**
+ * Extract the ClosureExpression from a script body.
+ * The filter literal parses as a script whose body is a single ClosureExpression.
+ */
+ private ClosureExpression extractClosure(Statement body) {
+ List<Statement> stmts = getStatements(body);
+ if (stmts.size() == 1 && stmts.get(0) instanceof ExpressionStatement) {
+ Expression expr = ((ExpressionStatement) stmts.get(0)).getExpression();
+ if (expr instanceof ClosureExpression) {
+ return (ClosureExpression) expr;
+ }
+ }
+ throw new IllegalStateException(
+ "Filter literal must be a single closure expression, got: "
+ + (stmts.isEmpty() ? "empty" : stmts.get(0).getClass().getSimpleName()));
+ }
+
+ /**
+ * Extract the boolean expression from a filter closure body.
+ * Handles the "wrapped block" pattern: { tags -> {compound} }
+ * where the inner {} is another ClosureExpression that must be unwrapped.
+ */
+ private Expression extractFilterBodyExpr(Statement code, String tagsVar) {
+ List<Statement> stmts = getStatements(code);
+ if (stmts.isEmpty()) {
+ throw new IllegalStateException("Empty filter closure body");
+ }
+
+ // Get the last statement's expression
+ Statement last = stmts.get(stmts.size() - 1);
+ Expression expr;
+ if (last instanceof ExpressionStatement) {
+ expr = ((ExpressionStatement) last).getExpression();
+ } else if (last instanceof ReturnStatement) {
+ expr = ((ReturnStatement) last).getExpression();
+ } else if (last instanceof BlockStatement) {
+ // Nested block — recurse to unwrap
+ return extractFilterBodyExpr(last, tagsVar);
+ } else {
+ throw new UnsupportedOperationException(
+ "Unsupported filter body statement: " + last.getClass().getSimpleName());
+ }
+
+ // Unwrap inner closure: { tags -> {compound} } → the inner closure body
+ if (expr instanceof ClosureExpression) {
+ ClosureExpression inner = (ClosureExpression) expr;
+ return extractFilterBodyExpr(inner.getCode(), tagsVar);
+ }
+
+ return expr;
+ }
+
+ // ---- AST Parsing ----
+
+ ModuleNode parseToAST(String expression) {
+ CompilerConfiguration cc = new CompilerConfiguration();
+ CompilationUnit cu = new CompilationUnit(cc);
+ cu.addSource("Script", expression);
+ cu.compile(Phases.CONVERSION);
+ List<ModuleNode> modules = cu.getAST().getModules();
+ if (modules.isEmpty()) {
+ throw new IllegalStateException("No AST modules produced for: " + expression);
+ }
+ return modules.get(0);
+ }
+
+ Statement extractBody(ModuleNode module) {
+ BlockStatement block = module.getStatementBlock();
+ if (block != null && !block.getStatements().isEmpty()) {
+ return block;
+ }
+ List<ClassNode> classes = module.getClasses();
+ if (!classes.isEmpty()) {
+ return module.getStatementBlock();
+ }
+ throw new IllegalStateException("Empty AST body");
+ }
+
+ // ---- Sample Name Collection ----
+
+ private void collectSampleNames(Statement stmt, Set<String> names) {
+ if (stmt instanceof BlockStatement) {
+ for (Statement s : ((BlockStatement) stmt).getStatements()) {
+ collectSampleNames(s, names);
+ }
+ } else if (stmt instanceof ExpressionStatement) {
+ collectSampleNamesFromExpr(((ExpressionStatement) stmt).getExpression(), names);
+ } else if (stmt instanceof ReturnStatement) {
+ collectSampleNamesFromExpr(((ReturnStatement) stmt).getExpression(), names);
+ }
+ }
+
+ void collectSampleNamesFromExpr(Expression expr, Set<String> names) {
+ if (expr instanceof VariableExpression) {
+ String name = ((VariableExpression) expr).getName();
+ if (!DOWNSAMPLING_CONSTANTS.contains(name)
+ && !KNOWN_TYPES.contains(name)
+ && !name.equals("this") && !name.equals("time")) {
+ names.add(name);
+ }
+ } else if (expr instanceof BinaryExpression) {
+ BinaryExpression bin = (BinaryExpression) expr;
+ collectSampleNamesFromExpr(bin.getLeftExpression(), names);
+ collectSampleNamesFromExpr(bin.getRightExpression(), names);
+ } else if (expr instanceof PropertyExpression) {
+ PropertyExpression pe = (PropertyExpression) expr;
+ collectSampleNamesFromExpr(pe.getObjectExpression(), names);
+ } else if (expr instanceof MethodCallExpression) {
+ MethodCallExpression mce = (MethodCallExpression) expr;
+ collectSampleNamesFromExpr(mce.getObjectExpression(), names);
+ collectSampleNamesFromExpr(mce.getArguments(), names);
+ } else if (expr instanceof ArgumentListExpression) {
+ for (Expression e : ((ArgumentListExpression) expr).getExpressions()) {
+ collectSampleNamesFromExpr(e, names);
+ }
+ } else if (expr instanceof TupleExpression) {
+ for (Expression e : ((TupleExpression) expr).getExpressions()) {
+ collectSampleNamesFromExpr(e, names);
+ }
+ }
+ }
+
+ // ---- Statement Visiting ----
+
+ String visitStatement(Statement stmt) {
+ if (stmt instanceof BlockStatement) {
+ List<Statement> stmts = ((BlockStatement) stmt).getStatements();
+ if (stmts.size() == 1) {
+ return visitStatement(stmts.get(0));
+ }
+ // Multi-statement: last one is the return value
+ return visitStatement(stmts.get(stmts.size() - 1));
+ } else if (stmt instanceof ExpressionStatement) {
+ return visitExpression(((ExpressionStatement) stmt).getExpression());
+ } else if (stmt instanceof ReturnStatement) {
+ return visitExpression(((ReturnStatement) stmt).getExpression());
+ }
+ throw new UnsupportedOperationException(
+ "Unsupported statement: " + stmt.getClass().getSimpleName());
+ }
+
+ // ---- Expression Visiting ----
+
+ String visitExpression(Expression expr) {
+ if (expr instanceof VariableExpression) {
+ return visitVariable((VariableExpression) expr);
+ } else if (expr instanceof ConstantExpression) {
+ return visitConstant((ConstantExpression) expr);
+ } else if (expr instanceof MethodCallExpression) {
+ return visitMethodCall((MethodCallExpression) expr);
+ } else if (expr instanceof PropertyExpression) {
+ return visitProperty((PropertyExpression) expr);
+ } else if (expr instanceof ListExpression) {
+ return visitList((ListExpression) expr);
+ } else if (expr instanceof BinaryExpression) {
+ return visitBinary((BinaryExpression) expr);
+ } else if (expr instanceof ClosureExpression) {
+ // Bare closure not in a recognized method context — shouldn't happen in MAL
+ throw new UnsupportedOperationException(
+ "Bare ClosureExpression outside method call context: " + expr.getText());
+ }
+ throw new UnsupportedOperationException(
+ "Unsupported expression (not yet implemented): "
+ + expr.getClass().getSimpleName() + " = " + expr.getText());
+ }
+
+ private String visitVariable(VariableExpression expr) {
+ String name = expr.getName();
+ if (DOWNSAMPLING_CONSTANTS.contains(name)) {
+ return "DownsamplingType." + name;
+ }
+ if (KNOWN_TYPES.contains(name)) {
+ return name;
+ }
+ if (name.equals("this")) {
+ return "this";
+ }
+ // Sample family lookup
+ return "samples.getOrDefault(\"" + escapeJava(name) + "\", SampleFamily.EMPTY)";
+ }
+
+ private String visitConstant(ConstantExpression expr) {
+ Object value = expr.getValue();
+ if (value == null) {
+ return "null";
+ }
+ if (value instanceof String) {
+ return "\"" + escapeJava((String) value) + "\"";
+ }
+ if (value instanceof Integer) {
+ return value.toString();
+ }
+ if (value instanceof Long) {
+ return value + "L";
+ }
+ if (value instanceof Double) {
+ return value.toString();
+ }
+ if (value instanceof Float) {
+ return value + "f";
+ }
+ if (value instanceof Boolean) {
+ return value.toString();
+ }
+ return value.toString();
+ }
+
+ // ---- MethodCall, Property, List ----
+
+ private String visitMethodCall(MethodCallExpression expr) {
+ String methodName = expr.getMethodAsString();
+ Expression objExpr = expr.getObjectExpression();
+ ArgumentListExpression args = toArgList(expr.getArguments());
+
+ // tag(closure) → TagFunction lambda
+ if ("tag".equals(methodName) && args.getExpressions().size() == 1
+ && args.getExpression(0) instanceof ClosureExpression) {
+ String obj = visitExpression(objExpr);
+ String lambda = visitTagClosure((ClosureExpression) args.getExpression(0));
+ return obj + ".tag((TagFunction) " + lambda + ")";
+ }
+
+ // forEach(list, closure) → ForEachFunction lambda
+ if ("forEach".equals(methodName) && args.getExpressions().size() == 2
+ && args.getExpression(1) instanceof ClosureExpression) {
+ String obj = visitExpression(objExpr);
+ String list = visitExpression(args.getExpression(0));
+ String lambda = visitForEachClosure((ClosureExpression) args.getExpression(1));
+ return obj + ".forEach(" + list + ", (ForEachFunction) " + lambda + ")";
+ }
+
+ // instance(..., closure) → last arg is PropertiesExtractor lambda (Step 9)
+ if ("instance".equals(methodName) && !args.getExpressions().isEmpty()) {
+ Expression lastArg = args.getExpression(args.getExpressions().size() - 1);
+ if (lastArg instanceof ClosureExpression) {
+ String obj = visitExpression(objExpr);
+ List<String> argStrs = new ArrayList<>();
+ for (int i = 0; i < args.getExpressions().size() - 1; i++) {
+ argStrs.add(visitExpression(args.getExpression(i)));
+ }
+ String lambda = visitPropertiesExtractorClosure((ClosureExpression) lastArg);
+ argStrs.add("(PropertiesExtractor) " + lambda);
+ return obj + ".instance(" + String.join(", ", argStrs) + ")";
+ }
+ }
+
+ String obj = visitExpression(objExpr);
+
+ // Static method calls: ClassExpression.method(...)
+ // e.g. ProcessRegistry.generateVirtualLocalProcess(...)
+ if (objExpr instanceof ClassExpression) {
+ String typeName = objExpr.getType().getNameWithoutPackage();
+ List<String> argStrs = visitArgList(args);
+ return typeName + "." + methodName + "(" + String.join(", ", argStrs) + ")";
+ }
+
+ // Regular instance method call: obj.method(args)
+ List<String> argStrs = visitArgList(args);
+ return obj + "." + methodName + "(" + String.join(", ", argStrs) + ")";
+ }
+
+ private String visitProperty(PropertyExpression expr) {
+ Expression obj = expr.getObjectExpression();
+ String prop = expr.getPropertyAsString();
+
+ // Enum access: Layer.GENERAL, DetectPoint.SERVER, K8sRetagType.Pod2Service
+ // At CONVERSION phase, Groovy doesn't resolve types, so these appear as
+ // PropertyExpression(VariableExpression("Layer"), "GENERAL") not ClassExpression.
+ if (obj instanceof ClassExpression) {
+ return obj.getType().getNameWithoutPackage() + "." + prop;
+ }
+ if (obj instanceof VariableExpression) {
+ String varName = ((VariableExpression) obj).getName();
+ if (KNOWN_TYPES.contains(varName)) {
+ return varName + "." + prop;
+ }
+ }
+
+ // Fallback: obj.prop
+ return visitExpression(obj) + "." + prop;
+ }
+
+ private String visitList(ListExpression expr) {
+ List<String> elements = new ArrayList<>();
+ for (Expression e : expr.getExpressions()) {
+ elements.add(visitExpression(e));
+ }
+ return "List.of(" + String.join(", ", elements) + ")";
+ }
+
+ // ---- Binary Arithmetic ----
+
+ private String visitBinary(BinaryExpression expr) {
+ int opType = expr.getOperation().getType();
+
+ if (isArithmetic(opType)) {
+ return visitArithmetic(expr.getLeftExpression(), expr.getRightExpression(), opType);
+ }
+
+ // Non-arithmetic binary ops handled in closure-specific visitors
+ throw new UnsupportedOperationException(
+ "Unsupported binary operator (not yet implemented): "
+ + expr.getOperation().getText() + " in " + expr.getText());
+ }
+
+ /**
+ * Arithmetic with operand-swap logic per upstream ExpandoMetaClass:
+ * <pre>
+ * SF + SF → left.plus(right)
+ * SF - SF → left.minus(right)
+ * SF * SF → left.multiply(right)
+ * SF / SF → left.div(right)
+ * SF op N → sf.op(N)
+ * N + SF → sf.plus(N) (swap)
+ * N - SF → sf.minus(N).negative()
+ * N * SF → sf.multiply(N) (swap)
+ * N / SF → sf.newValue(v → N / v)
+ * N op N → plain arithmetic
+ * </pre>
+ */
+ private String visitArithmetic(Expression left, Expression right, int opType) {
+ boolean leftNum = isNumberLiteral(left);
+ boolean rightNum = isNumberLiteral(right);
+ String leftStr = visitExpression(left);
+ String rightStr = visitExpression(right);
+
+ if (leftNum && rightNum) {
+ // N op N — plain arithmetic
+ return "(" + leftStr + " " + opSymbol(opType) + " " + rightStr + ")";
+ }
+
+ if (!leftNum && rightNum) {
+ // SF op N
+ return leftStr + "." + opMethod(opType) + "(" + rightStr + ")";
+ }
+
+ if (leftNum && !rightNum) {
+ // N op SF — operand swap per ExpandoMetaClass
+ switch (opType) {
+ case Types.PLUS:
+ return rightStr + ".plus(" + leftStr + ")";
+ case Types.MINUS:
+ return rightStr + ".minus(" + leftStr + ").negative()";
+ case Types.MULTIPLY:
+ return rightStr + ".multiply(" + leftStr + ")";
+ case Types.DIVIDE:
+ return rightStr + ".newValue(v -> " + leftStr + " / v)";
+ default:
+ break;
+ }
+ }
+
+ // SF op SF
+ return leftStr + "." + opMethod(opType) + "(" + rightStr + ")";
+ }
+
+ private boolean isNumberLiteral(Expression expr) {
+ if (expr instanceof ConstantExpression) {
+ return ((ConstantExpression) expr).getValue() instanceof Number;
+ }
+ return false;
+ }
+
+ private boolean isArithmetic(int opType) {
+ return opType == Types.PLUS || opType == Types.MINUS
+ || opType == Types.MULTIPLY || opType == Types.DIVIDE;
+ }
+
+ private String opMethod(int opType) {
+ switch (opType) {
+ case Types.PLUS: return "plus";
+ case Types.MINUS: return "minus";
+ case Types.MULTIPLY: return "multiply";
+ case Types.DIVIDE: return "div";
+ default: return "???";
+ }
+ }
+
+ private String opSymbol(int opType) {
+ switch (opType) {
+ case Types.PLUS: return "+";
+ case Types.MINUS: return "-";
+ case Types.MULTIPLY: return "*";
+ case Types.DIVIDE: return "/";
+ default: return "?";
+ }
+ }
+
+ // ---- tag() Closure ----
+
+ /**
+ * Translate a tag closure to a TagFunction lambda.
+ * Input: {tags -> tags.key = 'val'; tags.remove('x'); ...}
+ * Output: (tags -> { tags.put("key", "val"); tags.remove("x"); ... return tags; })
+ */
+ private String visitTagClosure(ClosureExpression closure) {
+ Parameter[] params = closure.getParameters();
+ String tagsVar = (params != null && params.length > 0) ? params[0].getName() : "tags";
+ List<Statement> stmts = getStatements(closure.getCode());
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("(").append(tagsVar).append(" -> {\n");
+ for (Statement s : stmts) {
+ sb.append(" ").append(visitTagStatement(s, tagsVar)).append("\n");
+ }
+ sb.append(" return ").append(tagsVar).append(";\n");
+ sb.append(" })");
+ return sb.toString();
+ }
+
+ private String visitTagStatement(Statement stmt, String tagsVar) {
+ if (stmt instanceof ExpressionStatement) {
+ return visitTagExpr(((ExpressionStatement) stmt).getExpression(), tagsVar) + ";";
+ }
+ if (stmt instanceof ReturnStatement) {
+ return "return " + tagsVar + ";";
+ }
+ if (stmt instanceof IfStatement) {
+ return visitTagIf((IfStatement) stmt, tagsVar);
+ }
+ throw new UnsupportedOperationException(
+ "Unsupported tag closure statement: " + stmt.getClass().getSimpleName());
+ }
+
+ // ---- If/Else + Compound Conditions in tag() ----
+
+ /**
+ * Translate an if/else inside a tag closure.
+ * <pre>
+ * if (tags['gc'] == 'Copy' || tags['gc'] == 'ParNew') { tags.gc = 'young' }
+ * else { tags.gc = 'old' }
+ * </pre>
+ * → Java if/else with .equals() for string comparisons.
+ */
+ private String visitTagIf(IfStatement ifStmt, String tagsVar) {
+ String condition = visitTagCondition(ifStmt.getBooleanExpression().getExpression(), tagsVar);
+ List<Statement> ifBody = getStatements(ifStmt.getIfBlock());
+ Statement elseBlock = ifStmt.getElseBlock();
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("if (").append(condition).append(") {\n");
+ for (Statement s : ifBody) {
+ sb.append(" ").append(visitTagStatement(s, tagsVar)).append("\n");
+ }
+ sb.append(" }");
+
+ if (elseBlock != null && !(elseBlock instanceof EmptyStatement)) {
+ sb.append(" else {\n");
+ List<Statement> elseBody = getStatements(elseBlock);
+ for (Statement s : elseBody) {
+ sb.append(" ").append(visitTagStatement(s, tagsVar)).append("\n");
+ }
+ sb.append(" }");
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Translate a condition expression inside a tag closure.
+ * Handles ==, !=, ||, && comparisons against tag values.
+ * Groovy == translates to .equals() in Java for null-safety.
+ */
+ private String visitTagCondition(Expression expr, String tagsVar) {
+ if (expr instanceof BinaryExpression) {
+ BinaryExpression bin = (BinaryExpression) expr;
+ int opType = bin.getOperation().getType();
+
+ if (opType == Types.COMPARE_EQUAL) {
+ return visitTagEquals(bin.getLeftExpression(), bin.getRightExpression(), tagsVar, false);
+ }
+ if (opType == Types.COMPARE_NOT_EQUAL) {
+ return visitTagEquals(bin.getLeftExpression(), bin.getRightExpression(), tagsVar, true);
+ }
+ if (opType == Types.LOGICAL_OR) {
+ return visitTagCondition(bin.getLeftExpression(), tagsVar)
+ + " || " + visitTagCondition(bin.getRightExpression(), tagsVar);
+ }
+ if (opType == Types.LOGICAL_AND) {
+ return visitTagCondition(bin.getLeftExpression(), tagsVar)
+ + " && " + visitTagCondition(bin.getRightExpression(), tagsVar);
+ }
+ }
+ if (expr instanceof BooleanExpression) {
+ return visitTagCondition(((BooleanExpression) expr).getExpression(), tagsVar);
+ }
+ // Fallback: treat as a tag value expression (truthiness)
+ return visitTagValue(expr, tagsVar);
+ }
+
+ /**
+ * Translate == or != comparison in tag context.
+ * Puts constant string on the left for null-safety:
+ * tags['gc'] == 'Copy' → "Copy".equals(tags.get("gc"))
+ * tags['gc'] != 'idle' → !"idle".equals(tags.get("gc"))
+ */
+ private String visitTagEquals(Expression left, Expression right, String tagsVar, boolean negate) {
+ // Null comparison: expr == null or expr != null
+ if (isNullConstant(right)) {
+ String leftStr = visitTagValue(left, tagsVar);
+ return negate ? leftStr + " != null" : leftStr + " == null";
+ }
+ if (isNullConstant(left)) {
+ String rightStr = visitTagValue(right, tagsVar);
+ return negate ? rightStr + " != null" : rightStr + " == null";
+ }
+
+ String leftStr = visitTagValue(left, tagsVar);
+ String rightStr = visitTagValue(right, tagsVar);
+
+ // If right side is a string constant, put it on left for null-safety
+ if (right instanceof ConstantExpression && ((ConstantExpression) right).getValue() instanceof String) {
+ String result = rightStr + ".equals(" + leftStr + ")";
+ return negate ? "!" + result : result;
+ }
+ // If left side is a string constant
+ if (left instanceof ConstantExpression && ((ConstantExpression) left).getValue() instanceof String) {
+ String result = leftStr + ".equals(" + rightStr + ")";
+ return negate ? "!" + result : result;
+ }
+ // Neither side is constant string — use Objects.equals
+ String result = "Objects.equals(" + leftStr + ", " + rightStr + ")";
+ return negate ? "!" + result : result;
+ }
+
+ // ---- Filter Conditions ----
+
+ /**
+ * Visit a condition expression in a filter context.
+ * Extends tag condition handling with:
+ * - {@code in} operator: {@code tags.key in ['a','b']} → {@code List.of("a","b").contains(tags.get("key"))}
+ * - Truthiness: bare {@code tags.key} → {@code tags.get("key") != null && !tags.get("key").isEmpty()}
+ * - Negation: {@code !tags.key} → {@code tags.get("key") == null || tags.get("key").isEmpty()}
+ */
+ private String visitFilterCondition(Expression expr, String tagsVar) {
+ // NotExpression: !tags.key → truthiness negation
+ if (expr instanceof NotExpression) {
+ Expression inner = ((NotExpression) expr).getExpression();
+ String val = visitTagValue(inner, tagsVar);
+ return "(" + val + " == null || " + val + ".isEmpty())";
+ }
+
+ if (expr instanceof BinaryExpression) {
+ BinaryExpression bin = (BinaryExpression) expr;
+ int opType = bin.getOperation().getType();
+
+ // Reuse == and != from tag conditions
+ if (opType == Types.COMPARE_EQUAL) {
+ return visitTagEquals(bin.getLeftExpression(), bin.getRightExpression(), tagsVar, false);
+ }
+ if (opType == Types.COMPARE_NOT_EQUAL) {
+ return visitTagEquals(bin.getLeftExpression(), bin.getRightExpression(), tagsVar, true);
+ }
+ // Logical operators — recurse with visitFilterCondition
+ if (opType == Types.LOGICAL_OR) {
+ return visitFilterCondition(bin.getLeftExpression(), tagsVar)
+ + " || " + visitFilterCondition(bin.getRightExpression(), tagsVar);
+ }
+ if (opType == Types.LOGICAL_AND) {
+ return visitFilterCondition(bin.getLeftExpression(), tagsVar)
+ + " && " + visitFilterCondition(bin.getRightExpression(), tagsVar);
+ }
+ // IN operator: tags.key in ['a', 'b'] → List.of("a","b").contains(tags.get("key"))
+ if (opType == Types.KEYWORD_IN) {
+ String val = visitTagValue(bin.getLeftExpression(), tagsVar);
+ String list = visitTagValue(bin.getRightExpression(), tagsVar);
+ return list + ".contains(" + val + ")";
+ }
+ }
+
+ if (expr instanceof BooleanExpression) {
+ return visitFilterCondition(((BooleanExpression) expr).getExpression(), tagsVar);
+ }
+
+ // Fallback: treat as truthiness check on a tag value
+ // tags.key → tags.get("key") != null && !tags.get("key").isEmpty()
+ String val = visitTagValue(expr, tagsVar);
+ return "(" + val + " != null && !" + val + ".isEmpty())";
+ }
+
+ /**
+ * Visit an expression inside a tag closure body.
+ * Handles assignment (put), remove, and other tag operations.
+ */
+ private String visitTagExpr(Expression expr, String tagsVar) {
+ // Assignment: tags.key = val or tags[key] = val
+ if (expr instanceof BinaryExpression) {
+ BinaryExpression bin = (BinaryExpression) expr;
+ if (bin.getOperation().getType() == Types.ASSIGN) {
+ return visitTagAssignment(bin.getLeftExpression(), bin.getRightExpression(), tagsVar);
+ }
+ }
+ // Method call: tags.remove('key')
+ if (expr instanceof MethodCallExpression) {
+ MethodCallExpression mce = (MethodCallExpression) expr;
+ if ("remove".equals(mce.getMethodAsString()) && isTagsVar(mce.getObjectExpression(), tagsVar)) {
+ ArgumentListExpression args = toArgList(mce.getArguments());
+ return tagsVar + ".remove(" + visitTagValue(args.getExpression(0), tagsVar) + ")";
+ }
+ }
+ return visitTagValue(expr, tagsVar);
+ }
+
+ /**
+ * Visit a tag assignment: LHS = RHS.
+ * tags.key = val → tags.put("key", val)
+ * tags[expr] = val → tags.put(expr, val)
+ */
+ private String visitTagAssignment(Expression left, Expression right, String tagsVar) {
+ String val = visitTagValue(right, tagsVar);
+
+ // tags.key = val → PropertyExpression
+ if (left instanceof PropertyExpression) {
+ PropertyExpression prop = (PropertyExpression) left;
+ if (isTagsVar(prop.getObjectExpression(), tagsVar)) {
+ return tagsVar + ".put(\"" + escapeJava(prop.getPropertyAsString()) + "\", " + val + ")";
+ }
+ }
+ // tags[expr] = val → BinaryExpression with LEFT_SQUARE_BRACKET
+ if (left instanceof BinaryExpression) {
+ BinaryExpression sub = (BinaryExpression) left;
+ if (sub.getOperation().getType() == Types.LEFT_SQUARE_BRACKET
+ && isTagsVar(sub.getLeftExpression(), tagsVar)) {
+ String key = visitTagValue(sub.getRightExpression(), tagsVar);
+ return tagsVar + ".put(" + key + ", " + val + ")";
+ }
+ }
+ throw new UnsupportedOperationException(
+ "Unsupported tag assignment target: " + left.getClass().getSimpleName() + " = " + left.getText());
+ }
+
+ /**
+ * Visit a value expression inside a tag closure.
+ * Translates tags.key → tags.get("key"), tags['key'] → tags.get("key"),
+ * string concat, method calls on tag values, etc.
+ */
+ String visitTagValue(Expression expr, String tagsVar) {
+ // tags.key → tags.get("key")
+ if (expr instanceof PropertyExpression) {
+ PropertyExpression prop = (PropertyExpression) expr;
+ if (isTagsVar(prop.getObjectExpression(), tagsVar)) {
+ return tagsVar + ".get(\"" + escapeJava(prop.getPropertyAsString()) + "\")";
+ }
+ // Not tags — could be enum like Layer.GENERAL
+ return visitProperty(prop);
+ }
+ // tags[expr] → tags.get(expr)
+ if (expr instanceof BinaryExpression) {
+ BinaryExpression bin = (BinaryExpression) expr;
+ if (bin.getOperation().getType() == Types.LEFT_SQUARE_BRACKET
+ && isTagsVar(bin.getLeftExpression(), tagsVar)) {
+ return tagsVar + ".get(" + visitTagValue(bin.getRightExpression(), tagsVar) + ")";
+ }
+ // String concatenation or other binary in tag context
+ if (bin.getOperation().getType() == Types.PLUS) {
+ return visitTagValue(bin.getLeftExpression(), tagsVar)
+ + " + " + visitTagValue(bin.getRightExpression(), tagsVar);
+ }
+ }
+ // Elvis operator — must check BEFORE TernaryExpression since it extends it
+ // expr ?: 'default' → (expr != null ? expr : "default")
+ if (expr instanceof ElvisOperatorExpression) {
+ ElvisOperatorExpression elvis = (ElvisOperatorExpression) expr;
+ String val = visitTagValue(elvis.getTrueExpression(), tagsVar);
+ String defaultVal = visitTagValue(elvis.getFalseExpression(), tagsVar);
+ return "(" + val + " != null ? " + val + " : " + defaultVal + ")";
+ }
+ // Ternary operator — tags.ApiId ? exprA : exprB
+ if (expr instanceof TernaryExpression) {
+ TernaryExpression tern = (TernaryExpression) expr;
+ String cond = visitFilterCondition(tern.getBooleanExpression().getExpression(), tagsVar);
+ String trueVal = visitTagValue(tern.getTrueExpression(), tagsVar);
+ String falseVal = visitTagValue(tern.getFalseExpression(), tagsVar);
+ return "(" + cond + " ? " + trueVal + " : " + falseVal + ")";
+ }
+ // Method call / safe navigation — tags['key']?.trim() → null check
+ if (expr instanceof MethodCallExpression) {
+ MethodCallExpression mce = (MethodCallExpression) expr;
+ String obj = visitTagValue(mce.getObjectExpression(), tagsVar);
+ ArgumentListExpression args = toArgList(mce.getArguments());
+ List<String> argStrs = new ArrayList<>();
+ for (Expression a : args.getExpressions()) {
+ argStrs.add(visitTagValue(a, tagsVar));
+ }
+ String call = obj + "." + mce.getMethodAsString() + "(" + String.join(", ", argStrs) + ")";
+ if (mce.isSafe()) {
+ return "(" + obj + " != null ? " + call + " : null)";
+ }
+ return call;
+ }
+ // Variable reference — could be tagsVar itself or a closure parameter
+ if (expr instanceof VariableExpression) {
+ String name = ((VariableExpression) expr).getName();
+ if (name.equals(tagsVar)) {
+ return tagsVar;
+ }
+ // Other variables: could be a local var or closure param
+ return name;
+ }
+ // Constant
+ if (expr instanceof ConstantExpression) {
+ return visitConstant((ConstantExpression) expr);
+ }
+ // List
+ if (expr instanceof ListExpression) {
+ return visitList((ListExpression) expr);
+ }
+ // Map literal — ['pod': tags.pod, 'namespace': tags.namespace]
+ if (expr instanceof MapExpression) {
+ MapExpression map = (MapExpression) expr;
+ List<String> entries = new ArrayList<>();
+ for (MapEntryExpression entry : map.getMapEntryExpressions()) {
+ entries.add(visitTagValue(entry.getKeyExpression(), tagsVar));
+ entries.add(visitTagValue(entry.getValueExpression(), tagsVar));
+ }
+ return "Map.of(" + String.join(", ", entries) + ")";
+ }
+ // Fallback
+ return visitExpression(expr);
+ }
+
+ private boolean isTagsVar(Expression expr, String tagsVar) {
+ return expr instanceof VariableExpression
+ && ((VariableExpression) expr).getName().equals(tagsVar);
+ }
+
+ private List<Statement> getStatements(Statement stmt) {
+ if (stmt instanceof BlockStatement) {
+ return ((BlockStatement) stmt).getStatements();
+ }
+ return List.of(stmt);
+ }
+
+ // ---- forEach() Closure ----
+
+ /**
+ * Translate a forEach closure to a ForEachFunction lambda.
+ * Input: forEach(['client','server'], {prefix, tags -> ...})
+ * Output: .forEach(List.of("client","server"), (ForEachFunction) (prefix, tags) -> { ... })
+ */
+ private String visitForEachClosure(ClosureExpression closure) {
+ Parameter[] params = closure.getParameters();
+ String prefixVar = (params != null && params.length > 0) ? params[0].getName() : "prefix";
+ String tagsVar = (params != null && params.length > 1) ? params[1].getName() : "tags";
+
+ List<Statement> stmts = getStatements(closure.getCode());
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("(").append(prefixVar).append(", ").append(tagsVar).append(") -> {\n");
+ for (Statement s : stmts) {
+ sb.append(" ").append(visitForEachStatement(s, tagsVar)).append("\n");
+ }
+ sb.append(" }");
+ return sb.toString();
+ }
+
+ private String visitForEachStatement(Statement stmt, String tagsVar) {
+ if (stmt instanceof ExpressionStatement) {
+ return visitForEachExpr(((ExpressionStatement) stmt).getExpression(), tagsVar) + ";";
+ }
+ if (stmt instanceof ReturnStatement) {
+ // forEach is void (BiConsumer) — return always has no value
+ return "return;";
+ }
+ if (stmt instanceof IfStatement) {
+ return visitForEachIf((IfStatement) stmt, tagsVar);
+ }
+ throw new UnsupportedOperationException(
+ "Unsupported forEach closure statement: " + stmt.getClass().getSimpleName());
+ }
+
+ /**
+ * Visit an expression inside a forEach closure body.
+ * Handles variable declarations, tag writes, local variable assignments.
+ */
+ private String visitForEachExpr(Expression expr, String tagsVar) {
+ // Variable declaration: String result = ""
+ if (expr instanceof DeclarationExpression) {
+ DeclarationExpression decl = (DeclarationExpression) expr;
+ String typeName = decl.getVariableExpression().getType().getNameWithoutPackage();
+ String varName = decl.getVariableExpression().getName();
+ String init = visitTagValue(decl.getRightExpression(), tagsVar);
+ return typeName + " " + varName + " = " + init;
+ }
+ // Assignment
+ if (expr instanceof BinaryExpression) {
+ BinaryExpression bin = (BinaryExpression) expr;
+ if (bin.getOperation().getType() == Types.ASSIGN) {
+ Expression left = bin.getLeftExpression();
+ // Tag write: tags.key = val or tags[expr] = val
+ if (isTagWrite(left, tagsVar)) {
+ return visitTagAssignment(left, bin.getRightExpression(), tagsVar);
+ }
+ // Local variable assignment: result = '129'
+ if (left instanceof VariableExpression) {
+ return ((VariableExpression) left).getName()
+ + " = " + visitTagValue(bin.getRightExpression(), tagsVar);
+ }
+ }
+ }
+ // Method call or other expression
+ return visitTagExpr(expr, tagsVar);
+ }
+
+ /**
+ * Translate if/else-if/else chains inside a forEach closure.
+ * Handles chained else-if by detecting IfStatement as else block.
+ */
+ private String visitForEachIf(IfStatement ifStmt, String tagsVar) {
+ String condition = visitTagCondition(ifStmt.getBooleanExpression().getExpression(), tagsVar);
+ List<Statement> ifBody = getStatements(ifStmt.getIfBlock());
+ Statement elseBlock = ifStmt.getElseBlock();
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("if (").append(condition).append(") {\n");
+ for (Statement s : ifBody) {
+ sb.append(" ").append(visitForEachStatement(s, tagsVar)).append("\n");
+ }
+ sb.append(" }");
+
+ if (elseBlock instanceof IfStatement) {
+ // else if chain
+ sb.append(" else ").append(visitForEachIf((IfStatement) elseBlock, tagsVar));
+ } else if (elseBlock != null && !(elseBlock instanceof EmptyStatement)) {
+ sb.append(" else {\n");
+ for (Statement s : getStatements(elseBlock)) {
+ sb.append(" ").append(visitForEachStatement(s, tagsVar)).append("\n");
+ }
+ sb.append(" }");
+ }
+
+ return sb.toString();
+ }
+
+ private boolean isTagWrite(Expression left, String tagsVar) {
+ if (left instanceof PropertyExpression) {
+ return isTagsVar(((PropertyExpression) left).getObjectExpression(), tagsVar);
+ }
+ if (left instanceof BinaryExpression) {
+ BinaryExpression sub = (BinaryExpression) left;
+ return sub.getOperation().getType() == Types.LEFT_SQUARE_BRACKET
+ && isTagsVar(sub.getLeftExpression(), tagsVar);
+ }
+ return false;
+ }
+
+ private boolean isNullConstant(Expression expr) {
+ return expr instanceof ConstantExpression && ((ConstantExpression) expr).getValue() == null;
+ }
+
+ // ---- PropertiesExtractor Closure ----
+
+ /**
+ * Translate a PropertiesExtractor closure.
+ * Input: { tags -> ['pod': tags.pod, 'namespace': tags.namespace] }
+ * Output: (tags -> Map.of("pod", tags.get("pod"), "namespace", tags.get("namespace")))
+ */
+ private String visitPropertiesExtractorClosure(ClosureExpression closure) {
+ Parameter[] params = closure.getParameters();
+ String tagsVar = (params != null && params.length > 0) ? params[0].getName() : "tags";
+ List<Statement> stmts = getStatements(closure.getCode());
+
+ Statement last = stmts.get(stmts.size() - 1);
+ Expression bodyExpr;
+ if (last instanceof ExpressionStatement) {
+ bodyExpr = ((ExpressionStatement) last).getExpression();
+ } else if (last instanceof ReturnStatement) {
+ bodyExpr = ((ReturnStatement) last).getExpression();
+ } else {
+ throw new UnsupportedOperationException(
+ "Unsupported PropertiesExtractor closure body: " + last.getClass().getSimpleName());
+ }
+ return "(" + tagsVar + " -> " + visitTagValue(bodyExpr, tagsVar) + ")";
+ }
+
+ // ---- Batch Registration, Compilation, and Manifest Writing ----
+
+ /**
+ * Register a transpiled expression for batch compilation.
+ */
+ public void registerExpression(String className, String source) {
+ expressionSources.put(className, source);
+ }
+
+ /**
+ * Register a transpiled filter for batch compilation.
+ */
+ public void registerFilter(String className, String filterLiteral, String source) {
+ filterSources.put(className, source);
+ filterLiteralToClass.put(filterLiteral, GENERATED_PACKAGE + "." + className);
+ }
+
+ /**
+ * Compile all registered sources using javax.tools.JavaCompiler.
+ *
+ * @param sourceDir directory to write .java source files (package dirs created automatically)
+ * @param outputDir directory for compiled .class files
+ * @param classpath classpath for javac (semicolon/colon-separated JAR paths)
+ * @throws IOException if file I/O fails
+ */
+ public void compileAll(File sourceDir, File outputDir, String classpath) throws IOException {
+ Map<String, String> allSources = new LinkedHashMap<>();
+ allSources.putAll(expressionSources);
+ allSources.putAll(filterSources);
+
+ if (allSources.isEmpty()) {
+ log.info("No MAL sources to compile.");
+ return;
+ }
+
+ // Write .java files
+ String packageDir = GENERATED_PACKAGE.replace('.', File.separatorChar);
+ File srcPkgDir = new File(sourceDir, packageDir);
+ if (!srcPkgDir.exists() && !srcPkgDir.mkdirs()) {
+ throw new IOException("Failed to create source dir: " + srcPkgDir);
+ }
+ if (!outputDir.exists() && !outputDir.mkdirs()) {
+ throw new IOException("Failed to create output dir: " + outputDir);
+ }
+
+ List<File> javaFiles = new ArrayList<>();
+ for (Map.Entry<String, String> entry : allSources.entrySet()) {
+ File javaFile = new File(srcPkgDir, entry.getKey() + ".java");
+ Files.writeString(javaFile.toPath(), entry.getValue());
+ javaFiles.add(javaFile);
+ }
+
+ // Compile
+ JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+ if (compiler == null) {
+ throw new IllegalStateException("No Java compiler available — requires JDK");
+ }
+
+ StringWriter errorWriter = new StringWriter();
+
+ try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) {
+ Iterable<? extends JavaFileObject> compilationUnits =
+ fileManager.getJavaFileObjectsFromFiles(javaFiles);
+
+ List<String> options = Arrays.asList(
+ "-d", outputDir.getAbsolutePath(),
+ "-classpath", classpath
+ );
+
+ JavaCompiler.CompilationTask task = compiler.getTask(
+ errorWriter, fileManager, null, options, null, compilationUnits);
+
+ boolean success = task.call();
+ if (!success) {
+ throw new RuntimeException(
+ "Java compilation failed for " + javaFiles.size() + " MAL sources:\n"
+ + errorWriter);
+ }
+ }
+
+ log.info("Compiled {} MAL sources to {}", allSources.size(), outputDir);
+ }
+
+ /**
+ * Write mal-expressions.txt manifest: one FQCN per line.
+ */
+ public void writeExpressionManifest(File outputDir) throws IOException {
+ File manifestDir = new File(outputDir, "META-INF");
+ if (!manifestDir.exists() && !manifestDir.mkdirs()) {
+ throw new IOException("Failed to create META-INF dir: " + manifestDir);
+ }
+
+ List<String> lines = expressionSources.keySet().stream()
+ .map(name -> GENERATED_PACKAGE + "." + name)
+ .collect(Collectors.toList());
+ Files.write(new File(manifestDir, "mal-expressions.txt").toPath(), lines);
+ log.info("Wrote mal-expressions.txt with {} entries", lines.size());
+ }
+
+ /**
+ * Write mal-filter-expressions.properties manifest: literal=FQCN.
+ */
+ public void writeFilterManifest(File outputDir) throws IOException {
+ File manifestDir = new File(outputDir, "META-INF");
+ if (!manifestDir.exists() && !manifestDir.mkdirs()) {
+ throw new IOException("Failed to create META-INF dir: " + manifestDir);
+ }
+
+ List<String> lines = filterLiteralToClass.entrySet().stream()
+ .map(e -> escapeProperties(e.getKey()) + "=" + e.getValue())
+ .collect(Collectors.toList());
+ Files.write(new File(manifestDir, "mal-filter-expressions.properties").toPath(), lines);
+ log.info("Wrote mal-filter-expressions.properties with {} entries", lines.size());
+ }
+
+ /** Escape a properties key (= and : need escaping). */
+ private static String escapeProperties(String s) {
+ return s.replace("\\", "\\\\")
+ .replace("=", "\\=")
+ .replace(":", "\\:")
+ .replace(" ", "\\ ");
+ }
+
+ // ---- Argument Utilities ----
+
+ private ArgumentListExpression toArgList(Expression args) {
+ if (args instanceof ArgumentListExpression) {
+ return (ArgumentListExpression) args;
+ }
+ if (args instanceof TupleExpression) {
+ ArgumentListExpression ale = new ArgumentListExpression();
+ for (Expression e : ((TupleExpression) args).getExpressions()) {
+ ale.addExpression(e);
+ }
+ return ale;
+ }
+ ArgumentListExpression ale = new ArgumentListExpression();
+ ale.addExpression(args);
+ return ale;
+ }
+
+ private List<String> visitArgList(ArgumentListExpression args) {
+ List<String> result = new ArrayList<>();
+ for (Expression arg : args.getExpressions()) {
+ result.add(visitExpression(arg));
+ }
+ return result;
+ }
+
+ // ---- Utility ----
+
+ static String escapeJava(String s) {
+ return s.replace("\\", "\\\\")
+ .replace("\"", "\\\"")
+ .replace("\n", "\\n")
+ .replace("\r", "\\r")
+ .replace("\t", "\\t");
+ }
+}
diff --git a/build-tools/precompiler/src/main/java/org/apache/skywalking/oap/server/buildtools/precompiler/Precompiler.java b/build-tools/precompiler/src/main/java/org/apache/skywalking/oap/server/buildtools/precompiler/Precompiler.java
index db3b0d2..6e7c2f1 100644
--- a/build-tools/precompiler/src/main/java/org/apache/skywalking/oap/server/buildtools/precompiler/Precompiler.java
+++ b/build-tools/precompiler/src/main/java/org/apache/skywalking/oap/server/buildtools/precompiler/Precompiler.java
@@ -197,6 +197,9 @@ public class Precompiler {
// ---- LAL pre-compilation ----
compileLAL(outputDir);
+ // ---- GraalVM native-image metadata generation ----
+ generateNativeImageConfig(outputDir);
+
log.info("Precompiler: done");
}
@@ -299,6 +302,9 @@ public class Precompiler {
DSL.getScriptRegistry().size(),
FilterExpression.getScriptRegistry().size());
+ // ---- MAL-to-Java transpilation ----
+ transpileMAL(outputDir);
+
// ---- Serialize config data as JSON for runtime loaders ----
serializeMALConfigData(outputDir, rulesByPath);
}
@@ -389,6 +395,69 @@ public class Precompiler {
return loadedRules;
}
+ /**
+ * Transpile MAL expressions and filters from Groovy to pure Java classes.
+ * Runs after MetricConvert pipeline — uses expression strings captured by DSL
+ * and filter literals captured by FilterExpression.
+ */
+ private static void transpileMAL(String outputDir) throws Exception {
+ MalToJavaTranspiler transpiler = new MalToJavaTranspiler();
+ int exprCount = 0;
+ int filterCount = 0;
+
+ // Transpile expressions: scriptName → expression string
+ for (Map.Entry<String, String> entry : DSL.getExpressionRegistry().entrySet()) {
+ String scriptName = entry.getKey();
+ String expression = entry.getValue();
+ String className = "MalExpr_" + scriptName;
+ try {
+ String source = transpiler.transpileExpression(className, expression);
+ transpiler.registerExpression(className, source);
+ exprCount++;
+ } catch (Exception e) {
+ log.warn("Failed to transpile MAL expression: {} = {}", scriptName, expression, e);
+ }
+ }
+
+ // Transpile filters: literal → Groovy class (we need literal for transpiler)
+ int filterIdx = 0;
+ for (String literal : FilterExpression.getScriptRegistry().keySet()) {
+ String className = "MalFilter_" + filterIdx++;
+ try {
+ String source = transpiler.transpileFilter(className, literal);
+ transpiler.registerFilter(className, literal, source);
+ filterCount++;
+ } catch (Exception e) {
+ log.warn("Failed to transpile MAL filter: {}", literal, e);
+ }
+ }
+
+ // Compile generated Java sources.
+ // Transpiled code targets GraalVM APIs (Java functional interfaces in SampleFamily,
+ // public ExpressionParsingContext.get()). Prepend meter-analyzer-for-graalvm classes
+ // to javac classpath so they take precedence over upstream meter-analyzer.
+ File javaSourceDir = new File(outputDir, "generated-mal-sources");
+ File javaOutputDir = new File(outputDir);
+ String classpath = resolveClasspath();
+ File graalvmMeterClasses = findGraalvmMeterClasses(outputDir);
+ if (graalvmMeterClasses != null) {
+ classpath = graalvmMeterClasses.getAbsolutePath() + File.pathSeparator + classpath;
+ log.info("MAL transpilation: prepended {} to javac classpath",
+ graalvmMeterClasses.getAbsolutePath());
+ } else {
+ log.warn("MAL transpilation: meter-analyzer-for-graalvm classes not found; "
+ + "transpiled sources may fail to compile");
+ }
+ transpiler.compileAll(javaSourceDir, javaOutputDir, classpath);
+
+ // Write transpiled manifests (alongside existing Groovy manifests)
+ transpiler.writeExpressionManifest(new File(outputDir));
+ transpiler.writeFilterManifest(new File(outputDir));
+
+ log.info("MAL transpilation: {} expressions, {} filters transpiled to Java",
+ exprCount, filterCount);
+ }
+
/**
* Pre-compile all LAL (Log Analysis Language) scripts.
* Uses the same CompilerConfiguration as upstream (CompileStatic + LALPrecompiledExtension,
@@ -678,4 +747,260 @@ public class Precompiler {
private static void writeManifest(Path path, List<String> lines) throws IOException {
Files.write(path, lines, StandardCharsets.UTF_8);
}
+
+ /**
+ * Generate GraalVM native-image metadata (reflect-config.json and resource-config.json)
+ * from the manifests already produced by the precompiler.
+ *
+ * This gives native-image 100% coverage of all pre-compiled classes — unlike the tracing
+ * agent which only captures code paths exercised during a traced run.
+ */
+ private static void generateNativeImageConfig(String outputDir) throws IOException {
+ Path nativeImageDir = Path.of(outputDir,
+ "META-INF", "native-image", "org.apache.skywalking", "oap-graalvm-distro");
+ Files.createDirectories(nativeImageDir);
+
+ generateReflectConfig(outputDir, nativeImageDir);
+ generateResourceConfig(nativeImageDir);
+ }
+
+ /**
+ * Generate reflect-config.json from all manifest files.
+ *
+ * Annotation-scanned classes get full reflection access (fields, methods, constructors)
+ * because AnnotationScan, StreamAnnotationListener, etc. inspect annotations and fields.
+ *
+ * Script/metric/dispatcher classes get constructor-only access since they are loaded
+ * via Class.forName() + getDeclaredConstructor().newInstance().
+ */
+ private static void generateReflectConfig(String outputDir, Path nativeImageDir) throws IOException {
+ Path metaInf = Path.of(outputDir, "META-INF");
+ Path annotationScanDir = metaInf.resolve("annotation-scan");
+
+ List<Map<String, Object>> entries = new ArrayList<>();
+
+ // Annotation-scanned classes — full reflection access
+ String[] fullAccessManifests = {
+ "ScopeDeclaration.txt", "Stream.txt", "Disable.txt", "MultipleDisable.txt",
+ "SourceDispatcher.txt", "ISourceDecorator.txt"
+ };
+ for (String manifest : fullAccessManifests) {
+ Path file = annotationScanDir.resolve(manifest);
+ if (Files.exists(file)) {
+ for (String className : readClassNames(file)) {
+ entries.add(fullAccessEntry(className));
+ }
+ }
+ }
+
+ // MeterFunction manifest — key=value format, full access (MeterSystem inspects annotations)
+ Path meterFunctionFile = annotationScanDir.resolve("MeterFunction.txt");
+ if (Files.exists(meterFunctionFile)) {
+ for (String className : readValueFromKeyValue(meterFunctionFile)) {
+ entries.add(fullAccessEntry(className));
+ }
+ }
+
+ // OAL metrics and dispatchers — constructor-only
+ addConstructorEntries(entries, metaInf.resolve("oal-metrics-classes.txt"));
+ addConstructorEntries(entries, metaInf.resolve("oal-dispatcher-classes.txt"));
+
+ // MAL Groovy scripts — key=value format, constructor-only
+ Path malScripts = metaInf.resolve("mal-groovy-scripts.txt");
+ if (Files.exists(malScripts)) {
+ for (String className : readValueFromKeyValue(malScripts)) {
+ entries.add(constructorOnlyEntry(className));
+ }
+ }
+
+ // MAL filter scripts — Properties format, constructor-only
+ Path filterScripts = metaInf.resolve("mal-filter-scripts.properties");
+ if (Files.exists(filterScripts)) {
+ Properties props = new Properties();
+ try (InputStream is = Files.newInputStream(filterScripts)) {
+ props.load(is);
+ }
+ List<String> filterClasses = new ArrayList<>(props.stringPropertyNames().stream()
+ .map(k -> props.getProperty(k))
+ .sorted()
+ .collect(Collectors.toList()));
+ for (String className : filterClasses) {
+ entries.add(constructorOnlyEntry(className));
+ }
+ }
+
+ // MAL transpiled expressions — one FQCN per line, constructor-only
+ addConstructorEntries(entries, metaInf.resolve("mal-expressions.txt"));
+
+ // MAL transpiled filters — Properties format, constructor-only
+ Path transpiledFilters = metaInf.resolve("mal-filter-expressions.properties");
+ if (Files.exists(transpiledFilters)) {
+ Properties tProps = new Properties();
+ try (InputStream is = Files.newInputStream(transpiledFilters)) {
+ tProps.load(is);
+ }
+ for (String className : tProps.stringPropertyNames().stream()
+ .map(tProps::getProperty).sorted().collect(Collectors.toList())) {
+ entries.add(constructorOnlyEntry(className));
+ }
+ }
+
+ // LAL scripts — key=value format, constructor-only
+ Path lalScripts = metaInf.resolve("lal-scripts-by-hash.txt");
+ if (Files.exists(lalScripts)) {
+ for (String className : readValueFromKeyValue(lalScripts)) {
+ entries.add(constructorOnlyEntry(className));
+ }
+ }
+
+ // MAL meter classes — key=value format, constructor-only
+ Path meterClasses = metaInf.resolve("mal-meter-classes.txt");
+ if (Files.exists(meterClasses)) {
+ for (String className : readValueFromKeyValue(meterClasses)) {
+ entries.add(constructorOnlyEntry(className));
+ }
+ }
+
+ // Write reflect-config.json
+ ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
+ mapper.writeValue(nativeImageDir.resolve("reflect-config.json").toFile(), entries);
+ log.info("Generated reflect-config.json with {} entries", entries.size());
+ }
+
+ /**
+ * Generate resource-config.json for all META-INF resources in the generated JAR.
+ */
+ private static void generateResourceConfig(Path nativeImageDir) throws IOException {
+ Map<String, Object> resourceConfig = new LinkedHashMap<>();
+ Map<String, Object> resources = new LinkedHashMap<>();
+ List<Map<String, String>> includes = new ArrayList<>();
+
+ includes.add(Map.of("pattern", "META-INF/annotation-scan/.*\\.txt"));
+ includes.add(Map.of("pattern", "META-INF/oal-.*\\.txt"));
+ includes.add(Map.of("pattern", "META-INF/mal-.*\\.txt"));
+ includes.add(Map.of("pattern", "META-INF/mal-.*\\.properties"));
+ includes.add(Map.of("pattern", "META-INF/lal-.*\\.txt"));
+ includes.add(Map.of("pattern", "META-INF/config-data/.*\\.json"));
+
+ resources.put("includes", includes);
+ resourceConfig.put("resources", resources);
+
+ ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
+ mapper.writeValue(nativeImageDir.resolve("resource-config.json").toFile(), resourceConfig);
+ log.info("Generated resource-config.json with {} resource patterns", includes.size());
+ }
+
+ private static Map<String, Object> fullAccessEntry(String className) {
+ Map<String, Object> entry = new LinkedHashMap<>();
+ entry.put("name", className);
+ entry.put("allDeclaredFields", true);
+ entry.put("allDeclaredMethods", true);
+ entry.put("allDeclaredConstructors", true);
+ return entry;
+ }
+
+ private static Map<String, Object> constructorOnlyEntry(String className) {
+ Map<String, Object> entry = new LinkedHashMap<>();
+ entry.put("name", className);
+ entry.put("methods", List.of(Map.of("name", "<init>", "parameterTypes", List.of())));
+ return entry;
+ }
+
+ /**
+ * Read class names from a manifest file (one class name per line).
+ */
+ private static List<String> readClassNames(Path file) throws IOException {
+ return Files.readAllLines(file, StandardCharsets.UTF_8).stream()
+ .map(String::trim)
+ .filter(s -> !s.isEmpty())
+ .sorted()
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Read values from a key=value manifest file. Returns sorted unique values.
+ */
+ private static List<String> readValueFromKeyValue(Path file) throws IOException {
+ return Files.readAllLines(file, StandardCharsets.UTF_8).stream()
+ .map(String::trim)
+ .filter(s -> !s.isEmpty() && s.contains("="))
+ .map(s -> s.substring(s.indexOf('=') + 1))
+ .distinct()
+ .sorted()
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Read class names from a one-per-line manifest and add constructor-only entries.
+ */
+ private static void addConstructorEntries(List<Map<String, Object>> entries, Path file) throws IOException {
+ if (Files.exists(file)) {
+ for (String className : readClassNames(file)) {
+ entries.add(constructorOnlyEntry(className));
+ }
+ }
+ }
+
+ /**
+ * Resolve the runtime classpath for javac compilation.
+ * exec-maven-plugin creates a URLClassLoader with project dependencies;
+ * extract URLs from it. Falls back to java.class.path system property.
+ */
+ @SuppressWarnings("deprecation")
+ /**
+ * Locate meter-analyzer-for-graalvm compiled classes directory.
+ * Tries multiple strategies: basedir property (set by surefire), outputDir traversal,
+ * and user.dir (CWD). Returns the directory if found, null otherwise.
+ */
+ private static File findGraalvmMeterClasses(String outputDir) {
+ String relPath = "oap-libs-for-graalvm/meter-analyzer-for-graalvm/target/classes";
+
+ // Strategy 1: basedir (set by surefire) → build-tools/precompiler/
+ String basedir = System.getProperty("basedir");
+ if (basedir != null) {
+ File candidate = new File(new File(basedir).getParentFile().getParentFile(), relPath);
+ if (candidate.isDirectory()) {
+ return candidate;
+ }
+ }
+
+ // Strategy 2: outputDir traversal (works when outputDir = target/generated-classes)
+ File candidate = new File(outputDir).getParentFile() // target/
+ .getParentFile() // precompiler/
+ .getParentFile() // build-tools/
+ .getParentFile(); // project root
+ candidate = new File(candidate, relPath);
+ if (candidate.isDirectory()) {
+ return candidate;
+ }
+
+ // Strategy 3: user.dir may be project root
+ candidate = new File(System.getProperty("user.dir"), relPath);
+ if (candidate.isDirectory()) {
+ return candidate;
+ }
+
+ return null;
+ }
+
+ private static String resolveClasspath() {
+ Set<String> paths = new java.util.LinkedHashSet<>();
+ ClassLoader cl = Thread.currentThread().getContextClassLoader();
+ while (cl != null) {
+ if (cl instanceof java.net.URLClassLoader) {
+ for (java.net.URL url : ((java.net.URLClassLoader) cl).getURLs()) {
+ try {
+ paths.add(new File(url.toURI()).getAbsolutePath());
+ } catch (Exception e) {
+ paths.add(url.getPath());
+ }
+ }
+ }
+ cl = cl.getParent();
+ }
+ if (!paths.isEmpty()) {
+ return String.join(File.pathSeparator, paths);
+ }
+ return System.getProperty("java.class.path", "");
+ }
}
diff --git a/build-tools/precompiler/src/test/java/org/apache/skywalking/oap/server/buildtools/precompiler/MalToJavaTranspilerTest.java b/build-tools/precompiler/src/test/java/org/apache/skywalking/oap/server/buildtools/precompiler/MalToJavaTranspilerTest.java
new file mode 100644
index 0000000..0bf4e9a
--- /dev/null
+++ b/build-tools/precompiler/src/test/java/org/apache/skywalking/oap/server/buildtools/precompiler/MalToJavaTranspilerTest.java
@@ -0,0 +1,982 @@
+/*
+ * 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.
+ */
+
+package org.apache.skywalking.oap.server.buildtools.precompiler;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for MalToJavaTranspiler.
+ */
+class MalToJavaTranspilerTest {
+
+ private MalToJavaTranspiler transpiler;
+
+ @BeforeEach
+ void setUp() {
+ transpiler = new MalToJavaTranspiler();
+ }
+
+ // ---- AST Parsing + Simple Variable References ----
+
+ @Test
+ void simpleVariableReference() {
+ String java = transpiler.transpileExpression("MalExpr_test", "metric_name");
+ assertNotNull(java);
+
+ // Verify package and class structure
+ assertTrue(java.contains("package " + MalToJavaTranspiler.GENERATED_PACKAGE),
+ "Should have correct package");
+ assertTrue(java.contains("public class MalExpr_test implements MalExpression"),
+ "Should implement MalExpression");
+ assertTrue(java.contains("public SampleFamily run(Map<String, SampleFamily> samples)"),
+ "Should have run method");
+
+ // Verify sample name tracking in ExpressionParsingContext
+ assertTrue(java.contains("ctx.getSamples().add(\"metric_name\")"),
+ "Should track sample name in parsing context");
+
+ // Verify sample lookup
+ assertTrue(java.contains("samples.getOrDefault(\"metric_name\", SampleFamily.EMPTY)"),
+ "Should look up sample family from map");
+ }
+
+ @Test
+ void downsamplingConstantNotTrackedAsSample() {
+ // DownsamplingType constants like SUM should not be treated as sample names
+ String java = transpiler.transpileExpression("MalExpr_test", "SUM");
+ assertNotNull(java);
+
+ // Should NOT track SUM as a sample name
+ assertTrue(!java.contains("ctx.getSamples().add(\"SUM\")"),
+ "Should not track DownsamplingType constant as sample");
+
+ // Should resolve to DownsamplingType.SUM
+ assertTrue(java.contains("DownsamplingType.SUM"),
+ "Should resolve to DownsamplingType.SUM");
+ }
+
+ @Test
+ void parseToAST_producesModuleNode() {
+ var ast = transpiler.parseToAST("some_metric");
+ assertNotNull(ast, "Should produce a ModuleNode");
+ assertNotNull(ast.getStatementBlock(), "Should have a statement block");
+ }
+
+ @Test
+ void constantString() {
+ // A bare string constant — not typical in MAL but tests ConstantExpression
+ String java = transpiler.transpileExpression("MalExpr_test", "'hello'");
+ assertNotNull(java);
+ assertTrue(java.contains("\"hello\""),
+ "Should convert Groovy string to Java string");
+ }
+
+ @Test
+ void constantNumber() {
+ String java = transpiler.transpileExpression("MalExpr_test", "42");
+ assertNotNull(java);
+ assertTrue(java.contains("42"),
+ "Should preserve number literal");
+ }
+
+ // ---- Method Chains + List Literals + Enum Properties ----
+
+ @Test
+ void simpleMethodChain() {
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric_name.sum(['a', 'b']).service(['svc'], Layer.GENERAL)");
+ assertNotNull(java);
+
+ assertTrue(java.contains(".sum(List.of(\"a\", \"b\"))"),
+ "Should translate ['a','b'] to List.of(\"a\", \"b\")");
+ assertTrue(java.contains(".service(List.of(\"svc\"), Layer.GENERAL)"),
+ "Should translate Layer.GENERAL as enum");
+ }
+
+ @Test
+ void tagEqualChain() {
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "cpu_seconds.tagNotEqual('mode', 'idle').sum(['host']).rate('PT1M')");
+ assertNotNull(java);
+
+ assertTrue(java.contains(".tagNotEqual(\"mode\", \"idle\")"),
+ "Should translate tagNotEqual with string args");
+ assertTrue(java.contains(".sum(List.of(\"host\"))"),
+ "Should translate single-element list");
+ assertTrue(java.contains(".rate(\"PT1M\")"),
+ "Should translate rate with string arg");
+ }
+
+ @Test
+ void downsamplingMethod() {
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.downsampling(SUM)");
+ assertNotNull(java);
+
+ assertTrue(java.contains(".downsampling(DownsamplingType.SUM)"),
+ "Should resolve SUM to DownsamplingType.SUM");
+ }
+
+ @Test
+ void retagByK8sMeta() {
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.retagByK8sMeta('service', K8sRetagType.Pod2Service, 'pod', 'namespace')");
+ assertNotNull(java);
+
+ assertTrue(java.contains(".retagByK8sMeta(\"service\", K8sRetagType.Pod2Service, \"pod\", \"namespace\")"),
+ "Should translate K8sRetagType enum and string args");
+ }
+
+ @Test
+ void histogramPercentile() {
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.sum(['le', 'svc']).histogram().histogram_percentile([50, 75, 90])");
+ assertNotNull(java);
+
+ assertTrue(java.contains(".histogram()"),
+ "Should translate no-arg histogram()");
+ assertTrue(java.contains(".histogram_percentile(List.of(50, 75, 90))"),
+ "Should translate integer list");
+ }
+
+ @Test
+ void sampleNameCollectionThroughChain() {
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "my_metric.sum(['a']).service(['svc'], Layer.GENERAL)");
+ assertNotNull(java);
+
+ assertTrue(java.contains("ctx.getSamples().add(\"my_metric\")"),
+ "Should collect sample name from root of method chain");
+ assertTrue(!java.contains("ctx.getSamples().add(\"a\")"),
+ "Should NOT collect 'a' (it's a string constant arg, not a sample)");
+ }
+
+ @Test
+ void detectPointEnum() {
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.serviceRelation(DetectPoint.CLIENT, ['src'], ['dst'], Layer.MESH_DP)");
+ assertNotNull(java);
+
+ assertTrue(java.contains("DetectPoint.CLIENT"),
+ "Should translate DetectPoint enum");
+ assertTrue(java.contains("Layer.MESH_DP"),
+ "Should translate Layer enum");
+ }
+
+ @Test
+ void enumImportsPresent() {
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.service(['svc'], Layer.GENERAL)");
+ assertNotNull(java);
+
+ assertTrue(java.contains("import org.apache.skywalking.oap.server.core.analysis.Layer;"),
+ "Should import Layer");
+ assertTrue(java.contains("import org.apache.skywalking.oap.server.core.source.DetectPoint;"),
+ "Should import DetectPoint");
+ assertTrue(java.contains("import org.apache.skywalking.oap.meter.analyzer.dsl.tagOpt.K8sRetagType;"),
+ "Should import K8sRetagType");
+ }
+
+ // ---- Binary Arithmetic with Operand-Swap ----
+
+ @Test
+ void sfTimesNumber() {
+ // metric * 100 → samples.getOrDefault(...).multiply(100)
+ String java = transpiler.transpileExpression("MalExpr_test", "metric * 100");
+ assertNotNull(java);
+ assertTrue(java.contains(".multiply(100)"),
+ "SF * N should call .multiply(N)");
+ }
+
+ @Test
+ void sfDivNumber() {
+ String java = transpiler.transpileExpression("MalExpr_test", "metric / 1024");
+ assertNotNull(java);
+ assertTrue(java.contains(".div(1024)"),
+ "SF / N should call .div(N)");
+ }
+
+ @Test
+ void numberMinusSf() {
+ // 100 - metric → metric.minus(100).negative()
+ String java = transpiler.transpileExpression("MalExpr_test", "100 - metric");
+ assertNotNull(java);
+ assertTrue(java.contains(".minus(100).negative()"),
+ "N - SF should produce sf.minus(N).negative()");
+ }
+
+ @Test
+ void numberDivSf() {
+ // 1 / metric → metric.newValue(v -> 1 / v)
+ String java = transpiler.transpileExpression("MalExpr_test", "1 / metric");
+ assertNotNull(java);
+ assertTrue(java.contains(".newValue(v -> 1 / v)"),
+ "N / SF should produce sf.newValue(v -> N / v)");
+ }
+
+ @Test
+ void numberPlusSf() {
+ // 10 + metric → metric.plus(10)
+ String java = transpiler.transpileExpression("MalExpr_test", "10 + metric");
+ assertNotNull(java);
+ assertTrue(java.contains(".plus(10)"),
+ "N + SF should swap to sf.plus(N)");
+ }
+
+ @Test
+ void numberTimesSf() {
+ // 100 * metric → metric.multiply(100)
+ String java = transpiler.transpileExpression("MalExpr_test", "100 * metric");
+ assertNotNull(java);
+ assertTrue(java.contains(".multiply(100)"),
+ "N * SF should swap to sf.multiply(N)");
+ }
+
+ @Test
+ void sfMinusSf() {
+ // metricA - metricB → metricA.minus(metricB)
+ String java = transpiler.transpileExpression("MalExpr_test", "mem_total - mem_avail");
+ assertNotNull(java);
+ assertTrue(java.contains("ctx.getSamples().add(\"mem_total\")"),
+ "Should collect both sample names");
+ assertTrue(java.contains("ctx.getSamples().add(\"mem_avail\")"),
+ "Should collect both sample names");
+ assertTrue(java.contains(".minus("),
+ "SF - SF should call .minus()");
+ }
+
+ @Test
+ void sfDivSfTimesNumber() {
+ // metricA / metricB * 100
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "used_bytes / max_bytes * 100");
+ assertNotNull(java);
+ assertTrue(java.contains(".div("),
+ "Should have .div() for SF / SF");
+ assertTrue(java.contains(".multiply(100)"),
+ "Should have .multiply(100) for result * 100");
+ }
+
+ @Test
+ void nestedParenArithmetic() {
+ // 100 - ((mem_free * 100) / mem_total)
+ // Parens are just AST precedence — this is:
+ // 100 - ( (mem_free * 100) / mem_total )
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "100 - ((mem_free * 100) / mem_total)");
+ assertNotNull(java);
+ // Inner: mem_free.multiply(100) → .div(mem_total) → then 100 - result
+ assertTrue(java.contains(".multiply(100)"),
+ "Should have inner multiply");
+ assertTrue(java.contains(".negative()"),
+ "100 - SF should produce .negative()");
+ }
+
+ @Test
+ void parenthesizedWithMethodChain() {
+ // (metric * 100).tagNotEqual('mode', 'idle').sum(['host']).rate('PT1M')
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "(metric * 100).tagNotEqual('mode', 'idle').sum(['host']).rate('PT1M')");
+ assertNotNull(java);
+ assertTrue(java.contains(".multiply(100)"),
+ "Should have multiply inside parens");
+ assertTrue(java.contains(".tagNotEqual(\"mode\", \"idle\")"),
+ "Should chain tagNotEqual after parens");
+ assertTrue(java.contains(".rate(\"PT1M\")"),
+ "Should chain rate at the end");
+ }
+
+ // ---- tag() Closure — Simple Cases ----
+
+ @Test
+ void tagAssignmentWithStringConcat() {
+ // .tag({tags -> tags.route = 'route/' + tags['route']})
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.tag({tags -> tags.route = 'route/' + tags['route']})");
+ assertNotNull(java);
+
+ assertTrue(java.contains(".tag((TagFunction)"),
+ "Should cast closure to TagFunction");
+ assertTrue(java.contains("tags.put(\"route\", \"route/\" + tags.get(\"route\"))"),
+ "Should translate assignment with string concat and subscript read");
+ assertTrue(java.contains("return tags;"),
+ "Should return tags at end of lambda");
+ }
+
+ @Test
+ void tagRemove() {
+ // .tag({tags -> tags.remove('condition')})
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.tag({tags -> tags.remove('condition')})");
+ assertNotNull(java);
+
+ assertTrue(java.contains("tags.remove(\"condition\")"),
+ "Should translate remove call");
+ }
+
+ @Test
+ void tagPropertyToProperty() {
+ // .tag({tags -> tags.rs_nm = tags.set})
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.tag({tags -> tags.rs_nm = tags.set})");
+ assertNotNull(java);
+
+ assertTrue(java.contains("tags.put(\"rs_nm\", tags.get(\"set\"))"),
+ "Should translate property read on RHS to tags.get()");
+ }
+
+ @Test
+ void tagStringConcatWithPropertyRead() {
+ // .tag({tags -> tags.cluster = 'es::' + tags.cluster})
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.tag({tags -> tags.cluster = 'es::' + tags.cluster})");
+ assertNotNull(java);
+
+ assertTrue(java.contains("tags.put(\"cluster\", \"es::\" + tags.get(\"cluster\"))"),
+ "Should translate string concat with property read");
+ }
+
+ @Test
+ void tagClosureLambdaStructure() {
+ // Verify the overall lambda structure
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.tag({tags -> tags.x = 'y'})");
+ assertNotNull(java);
+
+ assertTrue(java.contains("(tags -> {"),
+ "Should have lambda opening");
+ assertTrue(java.contains("return tags;"),
+ "Should return tags variable");
+ }
+
+ @Test
+ void tagWithSubscriptWrite() {
+ // .tag({tags -> tags['service_name'] = tags['svc']})
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.tag({tags -> tags['service_name'] = tags['svc']})");
+ assertNotNull(java);
+
+ assertTrue(java.contains("tags.put(\"service_name\", tags.get(\"svc\"))"),
+ "Should translate subscript write and read");
+ }
+
+ @Test
+ void tagChainAfterTag() {
+ // .tag({...}).sum(['host']).service(['svc'], Layer.GENERAL)
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.tag({tags -> tags.x = 'y'}).sum(['host']).service(['svc'], Layer.GENERAL)");
+ assertNotNull(java);
+
+ assertTrue(java.contains(".tag((TagFunction)"),
+ "Should have tag call");
+ assertTrue(java.contains(".sum(List.of(\"host\"))"),
+ "Should chain sum after tag");
+ assertTrue(java.contains(".service(List.of(\"svc\"), Layer.GENERAL)"),
+ "Should chain service after sum");
+ }
+
+ // ---- tag() Closure — if/else + Compound Conditions ----
+
+ @Test
+ void ifOnlyWithChainedOr() {
+ // Real pattern from otel-rules/oap.yaml: GC type classification
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.tag({tags -> if (tags['gc'] == 'PS Scavenge' || tags['gc'] == 'Copy' || tags['gc'] == 'ParNew' || tags['gc'] == 'G1 Young Generation') {tags.gc = 'young_gc_count'} })");
+ assertNotNull(java);
+
+ assertTrue(java.contains("if (\"PS Scavenge\".equals(tags.get(\"gc\"))"),
+ "Should translate first == with constant on left for null-safety");
+ assertTrue(java.contains("|| \"Copy\".equals(tags.get(\"gc\"))"),
+ "Should chain || for second comparison");
+ assertTrue(java.contains("|| \"ParNew\".equals(tags.get(\"gc\"))"),
+ "Should chain || for third comparison");
+ assertTrue(java.contains("|| \"G1 Young Generation\".equals(tags.get(\"gc\"))"),
+ "Should chain || for fourth comparison");
+ assertTrue(java.contains("tags.put(\"gc\", \"young_gc_count\")"),
+ "Should translate assignment in if body");
+ }
+
+ @Test
+ void ifElse() {
+ // Real pattern from elasticsearch-index.yaml: primary/replica classification
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.tag({tags -> if (tags['primary'] == 'true') {tags.primary = 'primary'} else {tags.primary = 'replica'} })");
+ assertNotNull(java);
+
+ assertTrue(java.contains("if (\"true\".equals(tags.get(\"primary\"))"),
+ "Should translate == comparison in condition");
+ assertTrue(java.contains("tags.put(\"primary\", \"primary\")"),
+ "Should translate if-branch assignment");
+ assertTrue(java.contains("} else {"),
+ "Should have else clause");
+ assertTrue(java.contains("tags.put(\"primary\", \"replica\")"),
+ "Should translate else-branch assignment");
+ }
+
+ @Test
+ void ifOnlyNoElse() {
+ // if-only, no else — the else block should be empty
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.tag({tags -> if (tags['level'] == '1') {tags.level = 'L1 aggregation'} })");
+ assertNotNull(java);
+
+ assertTrue(java.contains("if (\"1\".equals(tags.get(\"level\"))"),
+ "Should translate condition");
+ assertTrue(java.contains("tags.put(\"level\", \"L1 aggregation\")"),
+ "Should translate if-body");
+ assertTrue(!java.contains("else"),
+ "Should NOT have else clause");
+ }
+
+ @Test
+ void notEqualComparison() {
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.tag({tags -> if (tags['status'] != 'ok') {tags.status = 'error'} })");
+ assertNotNull(java);
+
+ assertTrue(java.contains("!\"ok\".equals(tags.get(\"status\"))"),
+ "Should translate != with negated .equals()");
+ }
+
+ @Test
+ void logicalAndCondition() {
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.tag({tags -> if (tags['a'] == 'x' && tags['b'] == 'y') {tags.c = 'z'} })");
+ assertNotNull(java);
+
+ assertTrue(java.contains("\"x\".equals(tags.get(\"a\")) && \"y\".equals(tags.get(\"b\"))"),
+ "Should translate && with .equals() on both sides");
+ }
+
+ @Test
+ void chainedTagClosuresWithIf() {
+ // Real pattern from oap.yaml: two chained tag closures
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.tag({tags -> if (tags['level'] == '1') {tags.level = 'L1 aggregation'} })" +
+ ".tag({tags -> if (tags['level'] == '2') {tags.level = 'L2 aggregation'} })");
+ assertNotNull(java);
+
+ assertTrue(java.contains("\"1\".equals(tags.get(\"level\"))"),
+ "Should translate first tag closure condition");
+ assertTrue(java.contains("\"2\".equals(tags.get(\"level\"))"),
+ "Should translate second tag closure condition");
+ }
+
+ @Test
+ void ifWithMethodChainAfter() {
+ // tag closure with if, followed by method chain
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.tag({tags -> if (tags['gc'] == 'Copy') {tags.gc = 'young'} }).sum(['host']).service(['svc'], Layer.GENERAL)");
+ assertNotNull(java);
+
+ assertTrue(java.contains(".tag((TagFunction)"),
+ "Should have TagFunction cast");
+ assertTrue(java.contains("\"Copy\".equals(tags.get(\"gc\"))"),
+ "Should have if condition");
+ assertTrue(java.contains(".sum(List.of(\"host\"))"),
+ "Should chain sum after tag");
+ }
+
+ // ---- Filter Closures ----
+
+ @Test
+ void simpleEqualityFilter() {
+ // Real pattern: { tags -> tags.job_name == 'vm-monitoring' }
+ String java = transpiler.transpileFilter("MalFilter_0",
+ "{ tags -> tags.job_name == 'vm-monitoring' }");
+ assertNotNull(java);
+
+ assertTrue(java.contains("public class MalFilter_0 implements MalFilter"),
+ "Should implement MalFilter");
+ assertTrue(java.contains("public boolean test(Map<String, String> tags)"),
+ "Should have test method");
+ assertTrue(java.contains("\"vm-monitoring\".equals(tags.get(\"job_name\"))"),
+ "Should translate == with constant on left");
+ }
+
+ @Test
+ void filterPackageAndImports() {
+ String java = transpiler.transpileFilter("MalFilter_0",
+ "{ tags -> tags.job_name == 'x' }");
+ assertNotNull(java);
+
+ assertTrue(java.contains("package " + MalToJavaTranspiler.GENERATED_PACKAGE),
+ "Should have correct package");
+ assertTrue(java.contains("import java.util.*;"),
+ "Should import java.util");
+ assertTrue(java.contains("import org.apache.skywalking.oap.meter.analyzer.dsl.*;"),
+ "Should import dsl package");
+ }
+
+ @Test
+ void orFilter() {
+ // Real pattern: tags.job_name == 'x' || tags.job_name == 'y'
+ String java = transpiler.transpileFilter("MalFilter_1",
+ "{ tags -> tags.job_name == 'flink-jobManager-monitoring' || tags.job_name == 'flink-taskManager-monitoring' }");
+ assertNotNull(java);
+
+ assertTrue(java.contains("\"flink-jobManager-monitoring\".equals(tags.get(\"job_name\"))"),
+ "Should translate first ==");
+ assertTrue(java.contains("|| \"flink-taskManager-monitoring\".equals(tags.get(\"job_name\"))"),
+ "Should translate || with second ==");
+ }
+
+ @Test
+ void inListFilter() {
+ // Real pattern: tags.job_name in ['kubernetes-cadvisor', 'kube-state-metrics']
+ String java = transpiler.transpileFilter("MalFilter_2",
+ "{ tags -> tags.job_name in ['kubernetes-cadvisor', 'kube-state-metrics'] }");
+ assertNotNull(java);
+
+ assertTrue(java.contains("List.of(\"kubernetes-cadvisor\", \"kube-state-metrics\").contains(tags.get(\"job_name\"))"),
+ "Should translate 'in' to List.of().contains()");
+ }
+
+ @Test
+ void compoundAndFilter() {
+ // Real pattern: tags.cloud_provider == 'aws' && tags.Namespace == 'AWS/S3'
+ String java = transpiler.transpileFilter("MalFilter_3",
+ "{ tags -> tags.cloud_provider == 'aws' && tags.Namespace == 'AWS/S3' }");
+ assertNotNull(java);
+
+ assertTrue(java.contains("\"aws\".equals(tags.get(\"cloud_provider\"))"),
+ "Should translate first ==");
+ assertTrue(java.contains("&& \"AWS/S3\".equals(tags.get(\"Namespace\"))"),
+ "Should translate && with second ==");
+ }
+
+ @Test
+ void truthinessFilter() {
+ // Real pattern: tags.ApiId used as boolean (truthiness)
+ String java = transpiler.transpileFilter("MalFilter_4",
+ "{ tags -> tags.cloud_provider == 'aws' && tags.Stage }");
+ assertNotNull(java);
+
+ assertTrue(java.contains("\"aws\".equals(tags.get(\"cloud_provider\"))"),
+ "Should translate ==");
+ assertTrue(java.contains("(tags.get(\"Stage\") != null && !tags.get(\"Stage\").isEmpty())"),
+ "Should translate bare tags.Stage as truthiness check");
+ }
+
+ @Test
+ void negatedTruthinessFilter() {
+ // Real pattern: !tags.Method — negated truthiness
+ String java = transpiler.transpileFilter("MalFilter_5",
+ "{ tags -> tags.cloud_provider == 'aws' && !tags.Method }");
+ assertNotNull(java);
+
+ assertTrue(java.contains("(tags.get(\"Method\") == null || tags.get(\"Method\").isEmpty())"),
+ "Should translate !tags.Method as negated truthiness");
+ }
+
+ @Test
+ void compoundWithTruthinessAndNegation() {
+ // Real AWS pattern: compound with ==, truthiness, and negation
+ String java = transpiler.transpileFilter("MalFilter_6",
+ "{ tags -> tags.cloud_provider == 'aws' && tags.Namespace == 'AWS/ApiGateway' && tags.Stage && !tags.Method }");
+ assertNotNull(java);
+
+ assertTrue(java.contains("\"aws\".equals(tags.get(\"cloud_provider\"))"),
+ "Should translate first ==");
+ assertTrue(java.contains("\"AWS/ApiGateway\".equals(tags.get(\"Namespace\"))"),
+ "Should translate second ==");
+ assertTrue(java.contains("(tags.get(\"Stage\") != null && !tags.get(\"Stage\").isEmpty())"),
+ "Should translate truthiness");
+ assertTrue(java.contains("(tags.get(\"Method\") == null || tags.get(\"Method\").isEmpty())"),
+ "Should translate negated truthiness");
+ }
+
+ @Test
+ void wrappedBlockFilter() {
+ // Real pattern: { tags -> {compound} } — inner {} needs unwrapping
+ String java = transpiler.transpileFilter("MalFilter_7",
+ "{ tags -> {tags.cloud_provider == 'aws' && tags.Namespace == 'AWS/S3'} }");
+ assertNotNull(java);
+
+ assertTrue(java.contains("\"aws\".equals(tags.get(\"cloud_provider\"))"),
+ "Should unwrap inner block and translate ==");
+ assertTrue(java.contains("\"AWS/S3\".equals(tags.get(\"Namespace\"))"),
+ "Should translate second == after unwrapping");
+ }
+
+ @Test
+ void truthinessWithOrInParens() {
+ // Real pattern: (tags.ApiId || tags.ApiName)
+ String java = transpiler.transpileFilter("MalFilter_8",
+ "{ tags -> tags.cloud_provider == 'aws' && (tags.ApiId || tags.ApiName) }");
+ assertNotNull(java);
+
+ assertTrue(java.contains("(tags.get(\"ApiId\") != null && !tags.get(\"ApiId\").isEmpty())"),
+ "Should translate ApiId truthiness");
+ assertTrue(java.contains("(tags.get(\"ApiName\") != null && !tags.get(\"ApiName\").isEmpty())"),
+ "Should translate ApiName truthiness");
+ }
+
+ // ---- forEach() Closure ----
+
+ @Test
+ void forEachBasicStructure() {
+ // Simple forEach with tag write
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.forEach(['client', 'server'], { prefix, tags -> tags[prefix + '_id'] = 'test' })");
+ assertNotNull(java);
+
+ assertTrue(java.contains(".forEach(List.of(\"client\", \"server\"), (ForEachFunction)"),
+ "Should cast closure to ForEachFunction");
+ assertTrue(java.contains("(prefix, tags) -> {"),
+ "Should have two-parameter lambda");
+ assertTrue(java.contains("tags.put(prefix + \"_id\", \"test\")"),
+ "Should translate dynamic subscript write");
+ }
+
+ @Test
+ void forEachNullCheckWithEarlyReturn() {
+ // Real pattern 1: null check with early return
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.forEach(['client'], { prefix, tags -> if (tags[prefix + '_process_id'] != null) { return } })");
+ assertNotNull(java);
+
+ assertTrue(java.contains("tags.get(prefix + \"_process_id\") != null"),
+ "Should translate null check");
+ assertTrue(java.contains("return;"),
+ "Should have void return for early exit");
+ }
+
+ @Test
+ void forEachWithProcessRegistry() {
+ // Real pattern: static method call with tag reads
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.forEach(['client'], { prefix, tags -> " +
+ "tags[prefix + '_process_id'] = ProcessRegistry.generateVirtualLocalProcess(tags.service, tags.instance) })");
+ assertNotNull(java);
+
+ assertTrue(java.contains("ProcessRegistry.generateVirtualLocalProcess(tags.get(\"service\"), tags.get(\"instance\"))"),
+ "Should translate static method call with tag reads as args");
+ assertTrue(java.contains("tags.put(prefix + \"_process_id\","),
+ "Should translate dynamic subscript write");
+ }
+
+ @Test
+ void forEachVarDeclaration() {
+ // Variable declaration inside forEach
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.forEach(['component'], { key, tags -> String result = '' })");
+ assertNotNull(java);
+
+ assertTrue(java.contains("String result = \"\""),
+ "Should translate variable declaration with empty string");
+ }
+
+ @Test
+ void forEachVarDeclWithTagRead() {
+ // Variable declaration with tag value initialization
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.forEach(['component'], { key, tags -> String protocol = tags['protocol'] })");
+ assertNotNull(java);
+
+ assertTrue(java.contains("String protocol = tags.get(\"protocol\")"),
+ "Should translate var decl with tag read");
+ }
+
+ @Test
+ void forEachIfElseIfChain() {
+ // Real pattern 2: chained if/else-if/else with local variables
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.forEach(['component'], { key, tags -> " +
+ "String protocol = tags['protocol']\n" +
+ "String ssl = tags['is_ssl']\n" +
+ "String result = ''\n" +
+ "if (protocol == 'http' && ssl == 'true') { result = '129' } " +
+ "else if (protocol == 'http') { result = '49' } " +
+ "else if (ssl == 'true') { result = '130' } " +
+ "else { result = '110' }\n" +
+ "tags[key] = result })");
+ assertNotNull(java);
+
+ // Variable declarations
+ assertTrue(java.contains("String protocol = tags.get(\"protocol\")"),
+ "Should declare protocol");
+ assertTrue(java.contains("String ssl = tags.get(\"is_ssl\")"),
+ "Should declare ssl");
+
+ // If condition with local vars
+ assertTrue(java.contains("\"http\".equals(protocol)"),
+ "Should compare local var with .equals()");
+ assertTrue(java.contains("\"true\".equals(ssl)"),
+ "Should compare ssl with .equals()");
+
+ // Else-if chain (not nested else { if ... })
+ assertTrue(java.contains("} else if ("),
+ "Should produce else-if, not nested else { if }");
+
+ // Final else
+ assertTrue(java.contains("} else {"),
+ "Should have final else");
+ assertTrue(java.contains("result = \"110\""),
+ "Should assign default value in else");
+
+ // Tag write with variable key
+ assertTrue(java.contains("tags.put(key, result)"),
+ "Should write result to tags[key]");
+ }
+
+ @Test
+ void forEachLocalVarAssignment() {
+ // Local variable reassignment
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.forEach(['x'], { key, tags -> " +
+ "String r = ''\n" +
+ "r = 'abc'\n" +
+ "tags[key] = r })");
+ assertNotNull(java);
+
+ assertTrue(java.contains("r = \"abc\""),
+ "Should translate local var reassignment");
+ }
+
+ @Test
+ void forEachEqualsOnStringComparison() {
+ // Equality with dynamic subscript in condition
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.forEach(['client'], { prefix, tags -> " +
+ "if (tags[prefix + '_local'] == 'true') { tags[prefix + '_id'] = 'local' } })");
+ assertNotNull(java);
+
+ assertTrue(java.contains("\"true\".equals(tags.get(prefix + \"_local\"))"),
+ "Should translate dynamic subscript comparison with .equals()");
+ assertTrue(java.contains("tags.put(prefix + \"_id\", \"local\")"),
+ "Should translate dynamic subscript assignment");
+ }
+
+ @Test
+ void chainedForEach() {
+ // Two forEach calls chained
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.forEach(['a'], { k1, tags -> tags[k1] = 'x' })" +
+ ".forEach(['b'], { k2, tags -> tags[k2] = 'y' })");
+ assertNotNull(java);
+
+ assertTrue(java.contains("(ForEachFunction) (k1, tags)"),
+ "Should have first forEach with k1");
+ assertTrue(java.contains("(ForEachFunction) (k2, tags)"),
+ "Should have second forEach with k2");
+ }
+
+ // ---- Elvis (?:), Safe Navigation (?.), Ternary (? :) ----
+
+ @Test
+ void safeNavigation() {
+ // tags['skywalking_service']?.trim()
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.tag({tags -> tags.svc = tags['skywalking_service']?.trim() })");
+ assertNotNull(java);
+
+ assertTrue(java.contains("(tags.get(\"skywalking_service\") != null ? tags.get(\"skywalking_service\").trim() : null)"),
+ "Should translate ?.trim() to null-checked call");
+ }
+
+ @Test
+ void elvisOperator() {
+ // expr ?: 'default'
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.tag({tags -> tags.svc = tags['name'] ?: 'unknown' })");
+ assertNotNull(java);
+
+ assertTrue(java.contains("(tags.get(\"name\") != null ? tags.get(\"name\") : \"unknown\")"),
+ "Should translate ?: to null-check with default");
+ }
+
+ @Test
+ void safeNavPlusElvis() {
+ // Real pattern: tags['skywalking_service']?.trim()?:'APISIX'
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.tag({tags -> tags.service_name = 'APISIX::'+(tags['skywalking_service']?.trim()?:'APISIX') })");
+ assertNotNull(java);
+
+ // The safe nav produces a null-checked trim
+ assertTrue(java.contains("tags.get(\"skywalking_service\") != null ? tags.get(\"skywalking_service\").trim() : null"),
+ "Should have safe nav for trim");
+ // The elvis wraps it with default
+ assertTrue(java.contains("!= null ?") && java.contains(": \"APISIX\""),
+ "Should have elvis default to APISIX");
+ // String concat prefix
+ assertTrue(java.contains("\"APISIX::\" + "),
+ "Should have string prefix concatenation");
+ }
+
+ @Test
+ void ternaryOperator() {
+ // Real pattern: tags.ApiId ? exprA : exprB
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.tag({tags -> tags.service_name = tags.ApiId ? 'gw::'+tags.ApiId : 'gw::'+tags.ApiName })");
+ assertNotNull(java);
+
+ // Condition should be truthiness check
+ assertTrue(java.contains("tags.get(\"ApiId\") != null && !tags.get(\"ApiId\").isEmpty()"),
+ "Should translate ternary condition as truthiness check");
+ // True branch
+ assertTrue(java.contains("\"gw::\" + tags.get(\"ApiId\")"),
+ "Should have true branch expression");
+ // False branch
+ assertTrue(java.contains("\"gw::\" + tags.get(\"ApiName\")"),
+ "Should have false branch expression");
+ }
+
+ @Test
+ void safeNavInFilterCondition() {
+ // Real pattern: tags.Service?.trim() in filter
+ String java = transpiler.transpileFilter("MalFilter_test",
+ "{ tags -> tags.job_name == 'eks-monitoring' && tags.Service?.trim() }");
+ assertNotNull(java);
+
+ assertTrue(java.contains("\"eks-monitoring\".equals(tags.get(\"job_name\"))"),
+ "Should translate == comparison");
+ // tags.Service?.trim() in boolean context → truthiness of the safe nav result
+ assertTrue(java.contains("tags.get(\"Service\") != null ? tags.get(\"Service\").trim() : null"),
+ "Should have safe nav for Service?.trim()");
+ }
+
+ // ---- instance() with PropertiesExtractor, MapExpression ----
+
+ @Test
+ void instanceWithPropertiesExtractor() {
+ // Real pattern: instance([...], '::', [...], '', Layer.K8S_SERVICE, {tags -> ['pod': tags.pod]})
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.instance(['cluster', 'service'], '::', ['pod'], '', Layer.K8S_SERVICE, " +
+ "{tags -> ['pod': tags.pod, 'namespace': tags.namespace]})");
+ assertNotNull(java);
+
+ assertTrue(java.contains(".instance("),
+ "Should have instance call");
+ assertTrue(java.contains("(PropertiesExtractor)"),
+ "Should cast closure to PropertiesExtractor");
+ assertTrue(java.contains("Map.of(\"pod\", tags.get(\"pod\"), \"namespace\", tags.get(\"namespace\"))"),
+ "Should translate map literal to Map.of()");
+ }
+
+ @Test
+ void mapExpressionInTagValue() {
+ // Map literal standalone
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.instance(['svc'], ['inst'], Layer.GENERAL, {tags -> ['key': tags.val]})");
+ assertNotNull(java);
+
+ assertTrue(java.contains("Map.of(\"key\", tags.get(\"val\"))"),
+ "Should translate single-entry map");
+ assertTrue(java.contains("(PropertiesExtractor)"),
+ "Should have PropertiesExtractor cast");
+ }
+
+ @Test
+ void processRelationNoClosures() {
+ // processRelation is just a regular method call — no special handling needed
+ String java = transpiler.transpileExpression("MalExpr_test",
+ "metric.processRelation('side', ['service'], ['instance'], " +
+ "'client_process_id', 'server_process_id', 'component')");
+ assertNotNull(java);
+
+ assertTrue(java.contains(".processRelation(\"side\", List.of(\"service\"), List.of(\"instance\"), " +
+ "\"client_process_id\", \"server_process_id\", \"component\")"),
+ "Should translate processRelation as regular method call");
+ }
+
+ // ---- Compilation + Manifests ----
+
+ @Test
+ void sourceWrittenForCompilation(@TempDir Path tempDir) throws Exception {
+ // Verify source files are written correctly (compilation needs full classpath
+ // including meter-analyzer-for-graalvm — tested in integration build)
+ String source = transpiler.transpileExpression("MalExpr_compile_test",
+ "metric.sum(['host']).service(['svc'], Layer.GENERAL)");
+ transpiler.registerExpression("MalExpr_compile_test", source);
+
+ File sourceDir = tempDir.resolve("src").toFile();
+ File outputDir = tempDir.resolve("classes").toFile();
+
+ // Try compilation — may fail if runtime classes not on classpath
+ try {
+ transpiler.compileAll(sourceDir, outputDir, System.getProperty("java.class.path"));
+
+ // If compilation succeeds, verify .class file
+ String classPath = MalToJavaTranspiler.GENERATED_PACKAGE.replace('.', File.separatorChar)
+ + File.separator + "MalExpr_compile_test.class";
+ assertTrue(new File(outputDir, classPath).exists(),
+ "Compiled .class file should exist");
+ } catch (RuntimeException e) {
+ if (e.getMessage().contains("compilation failed")) {
+ // Expected when MalExpression etc. not on test classpath
+ // Verify source was written correctly instead
+ String pkgPath = MalToJavaTranspiler.GENERATED_PACKAGE.replace('.', File.separatorChar);
+ File javaFile = new File(sourceDir, pkgPath + "/MalExpr_compile_test.java");
+ assertTrue(javaFile.exists(), "Source .java file should be written");
+ String written = Files.readString(javaFile.toPath());
+ assertTrue(written.contains("implements MalExpression"),
+ "Written source should implement MalExpression");
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ @Test
+ void expressionManifest(@TempDir Path tempDir) throws Exception {
+ transpiler.registerExpression("MalExpr_a",
+ transpiler.transpileExpression("MalExpr_a", "metric_a"));
+ transpiler.registerExpression("MalExpr_b",
+ transpiler.transpileExpression("MalExpr_b", "metric_b"));
+
+ File outputDir = tempDir.toFile();
+ transpiler.writeExpressionManifest(outputDir);
+
+ File manifest = new File(outputDir, "META-INF/mal-expressions.txt");
+ assertTrue(manifest.exists(), "Manifest file should exist");
+
+ List<String> lines = Files.readAllLines(manifest.toPath());
+ assertTrue(lines.contains(MalToJavaTranspiler.GENERATED_PACKAGE + ".MalExpr_a"),
+ "Should contain MalExpr_a FQCN");
+ assertTrue(lines.contains(MalToJavaTranspiler.GENERATED_PACKAGE + ".MalExpr_b"),
+ "Should contain MalExpr_b FQCN");
+ }
+
+ @Test
+ void filterManifest(@TempDir Path tempDir) throws Exception {
+ String literal = "{ tags -> tags.job == 'x' }";
+ transpiler.registerFilter("MalFilter_0", literal,
+ transpiler.transpileFilter("MalFilter_0", literal));
+
+ File outputDir = tempDir.toFile();
+ transpiler.writeFilterManifest(outputDir);
+
+ File manifest = new File(outputDir, "META-INF/mal-filter-expressions.properties");
+ assertTrue(manifest.exists(), "Filter manifest should exist");
+
+ String content = Files.readString(manifest.toPath());
+ assertTrue(content.contains(MalToJavaTranspiler.GENERATED_PACKAGE + ".MalFilter_0"),
+ "Should contain MalFilter_0 FQCN");
+ }
+}
diff --git a/oap-libs-for-graalvm/meter-analyzer-for-graalvm/pom.xml b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/pom.xml
index 3441483..2173682 100644
--- a/oap-libs-for-graalvm/meter-analyzer-for-graalvm/pom.xml
+++ b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/pom.xml
@@ -65,6 +65,11 @@
<exclude>org/apache/skywalking/oap/meter/analyzer/dsl/DSL$*.class</exclude>
<exclude>org/apache/skywalking/oap/meter/analyzer/dsl/FilterExpression.class</exclude>
<exclude>org/apache/skywalking/oap/meter/analyzer/dsl/FilterExpression$*.class</exclude>
+ <exclude>org/apache/skywalking/oap/meter/analyzer/dsl/SampleFamily.class</exclude>
+ <exclude>org/apache/skywalking/oap/meter/analyzer/dsl/SampleFamily$*.class</exclude>
+ <exclude>org/apache/skywalking/oap/meter/analyzer/dsl/ExpressionParsingContext.class</exclude>
+ <exclude>org/apache/skywalking/oap/meter/analyzer/dsl/ExpressionParsingContext$*.class</exclude>
+ <exclude>org/apache/skywalking/oap/meter/analyzer/dsl/EntityDescription/InstanceEntityDescription.class</exclude>
<exclude>org/apache/skywalking/oap/meter/analyzer/prometheus/rule/Rules.class</exclude>
</excludes>
</filter>
diff --git a/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/ExpressionParsingContext.java b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/ExpressionParsingContext.java
new file mode 100644
index 0000000..2f8b5a1
--- /dev/null
+++ b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/ExpressionParsingContext.java
@@ -0,0 +1,100 @@
+/*
+ * 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.
+ */
+
+package org.apache.skywalking.oap.meter.analyzer.dsl;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import java.io.Closeable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import lombok.Builder;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.skywalking.oap.server.core.analysis.meter.ScopeType;
+
+/**
+ * Same-FQCN replacement for ExpressionParsingContext.
+ * Makes get() and create() public so transpiled MalExpression classes
+ * (in package oal.rt.mal) can access them across packages.
+ *
+ * Canonical upstream: meter-analyzer (package-private access).
+ */
+@Getter
+@ToString
+@EqualsAndHashCode
+@Builder
+public class ExpressionParsingContext implements Closeable {
+
+ public static ExpressionParsingContext create() {
+ if (CACHE.get() == null) {
+ CACHE.set(ExpressionParsingContext.builder()
+ .samples(Lists.newArrayList())
+ .downsampling(DownsamplingType.AVG)
+ .scopeLabels(Sets.newHashSet())
+ .aggregationLabels(Sets.newHashSet()).build());
+ }
+ return CACHE.get();
+ }
+
+ public static Optional<ExpressionParsingContext> get() {
+ return Optional.ofNullable(CACHE.get());
+ }
+
+ private static final ThreadLocal<ExpressionParsingContext> CACHE = new ThreadLocal<>();
+
+ List<String> samples;
+
+ boolean isHistogram;
+ int[] percentiles;
+
+ Set<String> aggregationLabels;
+
+ Set<String> scopeLabels;
+
+ DownsamplingType downsampling;
+
+ ScopeType scopeType;
+
+ /**
+ * Get labels no scope related.
+ *
+ * @return labels
+ */
+ public List<String> getLabels() {
+ List<String> result = new ArrayList<>(aggregationLabels);
+ result.removeAll(scopeLabels);
+ return result;
+ }
+
+ /**
+ * Validate context after parsing
+ * @param exp expression literal
+ */
+ public void validate(String exp) {
+ Preconditions.checkNotNull(scopeType, exp + ": one of service(), instance() or endpoint() should be invoke");
+ }
+
+ @Override
+ public void close() {
+ CACHE.remove();
+ }
+}
diff --git a/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/FilterExpression.java b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/FilterExpression.java
index 7e38792..35252da 100644
--- a/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/FilterExpression.java
+++ b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/FilterExpression.java
@@ -79,7 +79,7 @@ public class FilterExpression {
try {
Map<String, SampleFamily> result = new HashMap<>();
for (Map.Entry<String, SampleFamily> entry : sampleFamilies.entrySet()) {
- SampleFamily afterFilter = entry.getValue().filter(filterClosure);
+ SampleFamily afterFilter = entry.getValue().filter(tags -> filterClosure.call(tags));
if (!Objects.equals(afterFilter, SampleFamily.EMPTY)) {
result.put(entry.getKey(), afterFilter);
}
diff --git a/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/MalExpression.java b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/MalExpression.java
new file mode 100644
index 0000000..6cdc004
--- /dev/null
+++ b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/MalExpression.java
@@ -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.
+ */
+
+package org.apache.skywalking.oap.meter.analyzer.dsl;
+
+import java.util.Map;
+
+/**
+ * Interface for generated MAL (Meter Analysis Language) expression classes.
+ * Each MAL expression from rule YAML files is transpiled to a pure Java class
+ * implementing this interface at build time.
+ *
+ * Replaces the Groovy DelegatingScript approach — no Groovy runtime needed.
+ */
+@FunctionalInterface
+public interface MalExpression {
+
+ /**
+ * Execute the MAL expression against the given sample families.
+ *
+ * @param samples map of metric name → SampleFamily, replaces Groovy's
+ * propertyMissing() delegate lookup
+ * @return the resulting SampleFamily, or SampleFamily.EMPTY if no data
+ */
+ SampleFamily run(Map<String, SampleFamily> samples);
+}
diff --git a/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/MalFilter.java b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/MalFilter.java
new file mode 100644
index 0000000..3096d40
--- /dev/null
+++ b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/MalFilter.java
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+package org.apache.skywalking.oap.meter.analyzer.dsl;
+
+import java.util.Map;
+
+/**
+ * Interface for generated MAL filter expression classes.
+ * Each filter expression from rule YAML files is transpiled to a pure Java class
+ * implementing this interface at build time.
+ *
+ * Replaces the Groovy Closure<Boolean> approach — no Groovy runtime needed.
+ */
+@FunctionalInterface
+public interface MalFilter {
+
+ /**
+ * Test whether samples with the given tags should be included.
+ *
+ * @param tags the sample labels/tags to evaluate
+ * @return true if the sample should be kept, false to filter it out
+ */
+ boolean test(Map<String, String> tags);
+}
diff --git a/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/SampleFamily.java b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/SampleFamily.java
new file mode 100644
index 0000000..65423bc
--- /dev/null
+++ b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/SampleFamily.java
@@ -0,0 +1,913 @@
+/*
+ * 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.
+ */
+
+package org.apache.skywalking.oap.meter.analyzer.dsl;
+
+import static java.util.function.UnaryOperator.identity;
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.mapping;
+import static java.util.stream.Collectors.toList;
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.skywalking.oap.meter.analyzer.dsl.EntityDescription.EndpointEntityDescription;
+import org.apache.skywalking.oap.meter.analyzer.dsl.EntityDescription.EntityDescription;
+import org.apache.skywalking.oap.meter.analyzer.dsl.EntityDescription.InstanceEntityDescription;
+import org.apache.skywalking.oap.meter.analyzer.dsl.EntityDescription.ProcessEntityDescription;
+import org.apache.skywalking.oap.meter.analyzer.dsl.EntityDescription.ProcessRelationEntityDescription;
+import org.apache.skywalking.oap.meter.analyzer.dsl.EntityDescription.ServiceEntityDescription;
+import org.apache.skywalking.oap.meter.analyzer.dsl.EntityDescription.ServiceRelationEntityDescription;
+import org.apache.skywalking.oap.meter.analyzer.dsl.SampleFamilyFunctions.DecorateFunction;
+import org.apache.skywalking.oap.meter.analyzer.dsl.SampleFamilyFunctions.ForEachFunction;
+import org.apache.skywalking.oap.meter.analyzer.dsl.SampleFamilyFunctions.PropertiesExtractor;
+import org.apache.skywalking.oap.meter.analyzer.dsl.SampleFamilyFunctions.SampleFilter;
+import org.apache.skywalking.oap.meter.analyzer.dsl.SampleFamilyFunctions.TagFunction;
+import org.apache.skywalking.oap.meter.analyzer.dsl.tagOpt.K8sRetagType;
+import org.apache.skywalking.oap.server.core.Const;
+import org.apache.skywalking.oap.server.core.UnexpectedException;
+import org.apache.skywalking.oap.server.core.analysis.Layer;
+import org.apache.skywalking.oap.server.core.analysis.meter.MeterEntity;
+import org.apache.skywalking.oap.server.core.analysis.meter.ScopeType;
+import org.apache.skywalking.oap.server.core.source.DetectPoint;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.function.DoubleBinaryOperator;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import io.vavr.Function2;
+import io.vavr.Function3;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.skywalking.oap.server.library.util.StringUtil;
+
+/**
+ * GraalVM replacement for upstream SampleFamily.
+ * Original: skywalking/oap-server/analyzer/meter-analyzer/src/main/java/.../dsl/SampleFamily.java
+ * Repackaged into meter-analyzer-for-graalvm via maven-shade-plugin.
+ *
+ * Change: 5 methods changed from groovy.lang.Closure to Java functional interfaces:
+ * tag(Closure) → tag(TagFunction)
+ * filter(Closure) → filter(SampleFilter)
+ * forEach(List, Closure) → forEach(List, ForEachFunction)
+ * decorate(Closure) → decorate(DecorateFunction)
+ * instance(..., Closure) → instance(..., PropertiesExtractor)
+ * Why: Groovy Closure is incompatible with GraalVM native image.
+ */
+@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+@EqualsAndHashCode
+@ToString
+@Slf4j
+public class SampleFamily {
+ public static final SampleFamily EMPTY = new SampleFamily(new Sample[0], RunningContext.EMPTY);
+
+ static SampleFamily build(RunningContext ctx, Sample... samples) {
+ Preconditions.checkNotNull(samples);
+ Preconditions.checkArgument(samples.length > 0);
+ samples = Arrays.stream(samples).filter(sample -> !Double.isNaN(sample.getValue())).toArray(Sample[]::new);
+ if (samples.length == 0) {
+ return EMPTY;
+ }
+ return new SampleFamily(samples, Optional.ofNullable(ctx).orElseGet(RunningContext::instance));
+ }
+
+ public final Sample[] samples;
+
+ public final RunningContext context;
+
+ /* tag filter operations*/
+ public SampleFamily tagEqual(String... labels) {
+ return match(labels, InternalOps::stringComp);
+ }
+
+ public SampleFamily tagNotEqual(String... labels) {
+ return match(labels, (sv, lv) -> !InternalOps.stringComp(sv, lv));
+ }
+
+ public SampleFamily tagMatch(String... labels) {
+ return match(labels, String::matches);
+ }
+
+ public SampleFamily tagNotMatch(String... labels) {
+ return match(labels, (sv, lv) -> !sv.matches(lv));
+ }
+
+ /* value filter operations*/
+ public SampleFamily valueEqual(double compValue) {
+ return valueMatch(CompType.EQUAL, compValue, InternalOps::doubleComp);
+ }
+
+ public SampleFamily valueNotEqual(double compValue) {
+ return valueMatch(CompType.NOT_EQUAL, compValue, InternalOps::doubleComp);
+ }
+
+ public SampleFamily valueGreater(double compValue) {
+ return valueMatch(CompType.GREATER, compValue, InternalOps::doubleComp);
+ }
+
+ public SampleFamily valueGreaterEqual(double compValue) {
+ return valueMatch(CompType.GREATER_EQUAL, compValue, InternalOps::doubleComp);
+ }
+
+ public SampleFamily valueLess(double compValue) {
+ return valueMatch(CompType.LESS, compValue, InternalOps::doubleComp);
+ }
+
+ public SampleFamily valueLessEqual(double compValue) {
+ return valueMatch(CompType.LESS_EQUAL, compValue, InternalOps::doubleComp);
+ }
+
+ /* Binary operator overloading*/
+ public SampleFamily plus(Number number) {
+ return newValue(v -> v + number.doubleValue());
+ }
+
+ public SampleFamily minus(Number number) {
+ return newValue(v -> v - number.doubleValue());
+ }
+
+ public SampleFamily multiply(Number number) {
+ return newValue(v -> v * number.doubleValue());
+ }
+
+ public SampleFamily div(Number number) {
+ return newValue(v -> v / number.doubleValue());
+ }
+
+ public SampleFamily negative() {
+ return newValue(v -> -v);
+ }
+
+ public SampleFamily plus(SampleFamily another) {
+ if (this == EMPTY && another == EMPTY) {
+ return SampleFamily.EMPTY;
+ }
+ if (this == EMPTY) {
+ return another;
+ }
+ if (another == EMPTY) {
+ return this;
+ }
+ return newValue(another, Double::sum);
+ }
+
+ public SampleFamily minus(SampleFamily another) {
+ if (this == EMPTY && another == EMPTY) {
+ return SampleFamily.EMPTY;
+ }
+ if (this == EMPTY) {
+ return another.negative();
+ }
+ if (another == EMPTY) {
+ return this;
+ }
+ return newValue(another, (a, b) -> a - b);
+ }
+
+ public SampleFamily multiply(SampleFamily another) {
+ if (this == EMPTY || another == EMPTY) {
+ return SampleFamily.EMPTY;
+ }
+ return newValue(another, (a, b) -> a * b);
+ }
+
+ public SampleFamily div(SampleFamily another) {
+ if (this == EMPTY) {
+ return SampleFamily.EMPTY;
+ }
+ if (another == EMPTY) {
+ return div(0.0);
+ }
+ return newValue(another, (a, b) -> a / b);
+ }
+
+ /* Aggregation operators */
+ public SampleFamily sum(List<String> by) {
+ return aggregate(by, Double::sum);
+ }
+
+ public SampleFamily max(List<String> by) {
+ return aggregate(by, Double::max);
+ }
+
+ public SampleFamily min(List<String> by) {
+ return aggregate(by, Double::min);
+ }
+
+ public SampleFamily avg(List<String> by) {
+ ExpressionParsingContext.get().ifPresent(ctx -> ctx.aggregationLabels.addAll(by));
+ if (this == EMPTY) {
+ return EMPTY;
+ }
+ if (by == null) {
+ double result = Arrays.stream(samples).mapToDouble(Sample::getValue).average().orElse(0.0D);
+ return SampleFamily.build(
+ this.context, InternalOps.newSample(samples[0].name, ImmutableMap.of(), samples[0].timestamp, result));
+ }
+
+ return SampleFamily.build(
+ this.context,
+ Arrays.stream(samples)
+ .collect(groupingBy(it -> InternalOps.getLabels(by, it), mapping(identity(), toList())))
+ .entrySet().stream()
+ .map(entry -> InternalOps.newSample(
+ entry.getValue().get(0).getName(),
+ entry.getKey(),
+ entry.getValue().get(0).getTimestamp(),
+ entry.getValue().stream().mapToDouble(Sample::getValue).average().orElse(0.0D)
+ ))
+ .toArray(Sample[]::new)
+ );
+ }
+
+ public SampleFamily count(List<String> by) {
+ ExpressionParsingContext.get().ifPresent(ctx -> ctx.aggregationLabels.addAll(by));
+ if (this == EMPTY) {
+ return EMPTY;
+ }
+ if (by == null) {
+ long result = Arrays.stream(samples).count();
+ return SampleFamily.build(
+ this.context, InternalOps.newSample(samples[0].name, ImmutableMap.of(), samples[0].timestamp, result));
+ }
+
+ if (by.size() == 1) {
+ Set<String> set = Arrays
+ .stream(samples)
+ .map(sample -> sample.labels.get(by.get(0)))
+ .filter(StringUtils::isNotBlank)
+ .collect(Collectors.toSet());
+
+ return SampleFamily.build(
+ this.context, InternalOps.newSample(samples[0].name, ImmutableMap.of(), samples[0].timestamp, set.size()));
+ }
+
+ Stream<Map.Entry<ImmutableMap<String, String>, List<Sample>>> stream = Arrays
+ .stream(samples)
+ .filter(sample -> sample.labels.keySet().containsAll(by))
+ .collect(groupingBy(it -> InternalOps.getLabels(by, it)))
+ .entrySet()
+ .stream()
+ .map(entry -> InternalOps.newSample(
+ entry.getValue().get(0).getName(),
+ entry.getKey(),
+ entry.getValue().get(0).getTimestamp(),
+ entry.getValue().size()))
+ .collect(groupingBy(it -> InternalOps.groupByExcludedLabel(by.get(by.size() - 1), it), mapping(identity(), toList())))
+ .entrySet()
+ .stream();
+
+ Sample[] array = stream
+ .map(entry -> InternalOps.newSample(
+ entry.getValue().get(0).getName(),
+ entry.getKey(),
+ entry.getValue().get(0).getTimestamp(),
+ entry.getValue().size()
+ ))
+ .toArray(Sample[]::new);
+
+ SampleFamily sampleFamily = SampleFamily.build(
+ this.context,
+ array
+ );
+ return sampleFamily;
+ }
+
+ protected SampleFamily aggregate(List<String> by, DoubleBinaryOperator aggregator) {
+ ExpressionParsingContext.get().ifPresent(ctx -> ctx.aggregationLabels.addAll(by));
+ if (this == EMPTY) {
+ return EMPTY;
+ }
+ if (by == null) {
+ double result = Arrays.stream(samples).mapToDouble(s -> s.value).reduce(aggregator).orElse(0.0D);
+ return SampleFamily.build(
+ this.context, InternalOps.newSample(samples[0].name, ImmutableMap.of(), samples[0].timestamp, result));
+ }
+ return SampleFamily.build(
+ this.context,
+ Arrays.stream(samples)
+ .collect(groupingBy(it -> InternalOps.getLabels(by, it), mapping(identity(), toList())))
+ .entrySet().stream()
+ .map(entry -> InternalOps.newSample(
+ entry.getValue().get(0).getName(),
+ entry.getKey(),
+ entry.getValue().get(0).getTimestamp(),
+ entry.getValue().stream().mapToDouble(Sample::getValue).reduce(aggregator).orElse(0.0D)
+ ))
+ .toArray(Sample[]::new)
+ );
+ }
+
+ /* Function */
+ public SampleFamily increase(String range) {
+ Preconditions.checkArgument(!Strings.isNullOrEmpty(range));
+ if (this == EMPTY) {
+ return EMPTY;
+ }
+ return SampleFamily.build(
+ this.context,
+ Arrays.stream(samples)
+ .map(sample -> sample.increase(
+ range,
+ context.metricName,
+ (lowerBoundValue, unused) -> sample.value - lowerBoundValue
+ ))
+ .toArray(Sample[]::new)
+ );
+ }
+
+ public SampleFamily rate(String range) {
+ Preconditions.checkArgument(!Strings.isNullOrEmpty(range));
+ if (this == EMPTY) {
+ return EMPTY;
+ }
+ return SampleFamily.build(
+ this.context,
+ Arrays.stream(samples)
+ .map(sample -> sample.increase(
+ range,
+ context.metricName,
+ (lowerBoundValue, lowerBoundTime) -> {
+ final long timeDiff = (sample.timestamp - lowerBoundTime) / 1000;
+ return timeDiff < 1L ? 0.0 : (sample.value - lowerBoundValue) / timeDiff;
+ }
+ ))
+ .toArray(Sample[]::new)
+ );
+ }
+
+ public SampleFamily irate() {
+ if (this == EMPTY) {
+ return EMPTY;
+ }
+ return SampleFamily.build(
+ this.context,
+ Arrays.stream(samples)
+ .map(sample -> sample.increase(
+ context.metricName,
+ (lowerBoundValue, lowerBoundTime) -> {
+ final long timeDiff = (sample.timestamp - lowerBoundTime) / 1000;
+ return timeDiff < 1L ? 0.0 : (sample.value - lowerBoundValue) / timeDiff;
+ }
+ ))
+ .toArray(Sample[]::new)
+ );
+ }
+
+ /**
+ * Tag transformation using a Java functional interface.
+ * Replaces upstream tag(Closure) — no Groovy Closure needed.
+ */
+ @SuppressWarnings(value = "unchecked")
+ public SampleFamily tag(TagFunction fn) {
+ if (this == EMPTY) {
+ return EMPTY;
+ }
+ return SampleFamily.build(
+ this.context,
+ Arrays.stream(samples)
+ .map(sample -> {
+ Map<String, String> arg = Maps.newHashMap(sample.labels);
+ Map<String, String> result = fn.apply(arg);
+ return sample.toBuilder()
+ .labels(ImmutableMap.copyOf(
+ Optional.ofNullable(result).orElse(arg)))
+ .build();
+ }).toArray(Sample[]::new)
+ );
+ }
+
+ /**
+ * Sample filtering using a Java functional interface.
+ * Replaces upstream filter(Closure) — no Groovy Closure needed.
+ */
+ public SampleFamily filter(SampleFilter filter) {
+ if (this == EMPTY) {
+ return EMPTY;
+ }
+ final Sample[] filtered = Arrays.stream(samples)
+ .filter(it -> filter.test(it.labels))
+ .toArray(Sample[]::new);
+ if (filtered.length == 0) {
+ return EMPTY;
+ }
+ return SampleFamily.build(context, filtered);
+ }
+
+ /* k8s retags*/
+ public SampleFamily retagByK8sMeta(String newLabelName,
+ K8sRetagType type,
+ String existingLabelName,
+ String namespaceLabelName) {
+ Preconditions.checkArgument(!Strings.isNullOrEmpty(newLabelName));
+ Preconditions.checkArgument(!Strings.isNullOrEmpty(existingLabelName));
+ Preconditions.checkArgument(!Strings.isNullOrEmpty(namespaceLabelName));
+ if (this == EMPTY) {
+ return EMPTY;
+ }
+
+ return SampleFamily.build(
+ this.context, type.execute(samples, newLabelName, existingLabelName, namespaceLabelName));
+ }
+
+ public SampleFamily histogram() {
+ return histogram("le", this.context.defaultHistogramBucketUnit);
+ }
+
+ public SampleFamily histogram(String le) {
+ return histogram(le, this.context.defaultHistogramBucketUnit);
+ }
+
+ public SampleFamily histogram(String le, TimeUnit unit) {
+ long scale = unit.toMillis(1);
+ Preconditions.checkArgument(scale > 0);
+ ExpressionParsingContext.get().ifPresent(ctx -> ctx.isHistogram = true);
+ if (this == EMPTY) {
+ return EMPTY;
+ }
+ return SampleFamily.build(
+ this.context,
+ Stream.concat(
+ Arrays.stream(samples).filter(s -> !s.labels.containsKey(le)),
+ Arrays.stream(samples)
+ .filter(s -> s.labels.containsKey(le))
+ .sorted(Comparator.comparingDouble(s -> Double.parseDouble(s.labels.get(le))))
+ .map(s -> {
+ double r = s.value;
+ ImmutableMap<String, String> ll = ImmutableMap.<String, String>builder()
+ .putAll(Maps.filterKeys(s.labels,
+ key -> !Objects.equals(
+ key, le)
+ ))
+ .put(
+ "le",
+ String.valueOf((long) ((Double.parseDouble(s.labels.get(le))) * scale)))
+ .build();
+ return InternalOps.newSample(s.name, ll, s.timestamp, r);
+ })
+ ).toArray(Sample[]::new)
+ );
+ }
+
+ public SampleFamily histogram_percentile(List<Integer> percentiles) {
+ Preconditions.checkArgument(percentiles.size() > 0);
+ int[] p = percentiles.stream().mapToInt(i -> i).toArray();
+ ExpressionParsingContext.get().ifPresent(ctx -> {
+ Preconditions.checkState(
+ ctx.isHistogram, "histogram() should be invoked before invoking histogram_percentile()");
+ ctx.percentiles = p;
+ });
+ return this;
+ }
+
+ public SampleFamily service(List<String> labelKeys, Layer layer) {
+ Preconditions.checkArgument(labelKeys.size() > 0);
+ ExpressionParsingContext.get().ifPresent(ctx -> {
+ ctx.scopeType = ScopeType.SERVICE;
+ ctx.scopeLabels.addAll(labelKeys);
+ });
+ if (this == EMPTY) {
+ return EMPTY;
+ }
+ return createMeterSamples(new ServiceEntityDescription(labelKeys, layer, Const.POINT));
+ }
+
+ public SampleFamily service(List<String> labelKeys, String delimiter, Layer layer) {
+ Preconditions.checkArgument(labelKeys.size() > 0);
+ ExpressionParsingContext.get().ifPresent(ctx -> {
+ ctx.scopeType = ScopeType.SERVICE;
+ ctx.scopeLabels.addAll(labelKeys);
+ });
+ if (this == EMPTY) {
+ return EMPTY;
+ }
+ return createMeterSamples(new ServiceEntityDescription(labelKeys, layer, delimiter));
+ }
+
+ /**
+ * Instance binding with properties extractor using a Java functional interface.
+ * Replaces upstream instance(..., Closure) — no Groovy Closure needed.
+ */
+ public SampleFamily instance(List<String> serviceKeys, String serviceDelimiter,
+ List<String> instanceKeys, String instanceDelimiter,
+ Layer layer, PropertiesExtractor propertiesExtractor) {
+ Preconditions.checkArgument(serviceKeys.size() > 0);
+ Preconditions.checkArgument(instanceKeys.size() > 0);
+ ExpressionParsingContext.get().ifPresent(ctx -> {
+ ctx.scopeType = ScopeType.SERVICE_INSTANCE;
+ ctx.scopeLabels.addAll(serviceKeys);
+ ctx.scopeLabels.addAll(instanceKeys);
+ });
+ if (this == EMPTY) {
+ return EMPTY;
+ }
+ return createMeterSamples(new InstanceEntityDescription(
+ serviceKeys, instanceKeys, layer, serviceDelimiter, instanceDelimiter, propertiesExtractor));
+ }
+
+ public SampleFamily instance(List<String> serviceKeys, List<String> instanceKeys, Layer layer) {
+ return instance(serviceKeys, Const.POINT, instanceKeys, Const.POINT, layer, null);
+ }
+
+ public SampleFamily endpoint(List<String> serviceKeys, List<String> endpointKeys, String delimiter, Layer layer) {
+ Preconditions.checkArgument(serviceKeys.size() > 0);
+ Preconditions.checkArgument(endpointKeys.size() > 0);
+ ExpressionParsingContext.get().ifPresent(ctx -> {
+ ctx.scopeType = ScopeType.ENDPOINT;
+ ctx.scopeLabels.addAll(serviceKeys);
+ ctx.scopeLabels.addAll(endpointKeys);
+ });
+ if (this == EMPTY) {
+ return EMPTY;
+ }
+ return createMeterSamples(new EndpointEntityDescription(serviceKeys, endpointKeys, layer, delimiter));
+ }
+
+ public SampleFamily endpoint(List<String> serviceKeys, List<String> endpointKeys, Layer layer) {
+ return endpoint(serviceKeys, endpointKeys, Const.POINT, layer);
+ }
+
+ public SampleFamily process(List<String> serviceKeys, List<String> serviceInstanceKeys, List<String> processKeys, String layerKey) {
+ Preconditions.checkArgument(serviceKeys.size() > 0);
+ Preconditions.checkArgument(serviceInstanceKeys.size() > 0);
+ Preconditions.checkArgument(processKeys.size() > 0);
+ ExpressionParsingContext.get().ifPresent(ctx -> {
+ ctx.scopeType = ScopeType.PROCESS;
+ ctx.scopeLabels.addAll(serviceKeys);
+ ctx.scopeLabels.addAll(serviceInstanceKeys);
+ ctx.scopeLabels.addAll(processKeys);
+ ctx.scopeLabels.add(layerKey);
+ });
+ if (this == EMPTY) {
+ return EMPTY;
+ }
+ return createMeterSamples(new ProcessEntityDescription(serviceKeys, serviceInstanceKeys, processKeys, layerKey, Const.POINT));
+ }
+
+ public SampleFamily serviceRelation(DetectPoint detectPoint, List<String> sourceServiceKeys, List<String> destServiceKeys, Layer layer) {
+ Preconditions.checkArgument(sourceServiceKeys.size() > 0);
+ Preconditions.checkArgument(destServiceKeys.size() > 0);
+ ExpressionParsingContext.get().ifPresent(ctx -> {
+ ctx.scopeType = ScopeType.SERVICE_RELATION;
+ ctx.scopeLabels.addAll(sourceServiceKeys);
+ ctx.scopeLabels.addAll(destServiceKeys);
+ });
+ if (this == EMPTY) {
+ return EMPTY;
+ }
+ return createMeterSamples(new ServiceRelationEntityDescription(sourceServiceKeys, destServiceKeys, detectPoint, layer, Const.POINT, null));
+ }
+
+ public SampleFamily serviceRelation(DetectPoint detectPoint, List<String> sourceServiceKeys, List<String> destServiceKeys, String delimiter, Layer layer, String componentIdKey) {
+ Preconditions.checkArgument(sourceServiceKeys.size() > 0);
+ Preconditions.checkArgument(destServiceKeys.size() > 0);
+ ExpressionParsingContext.get().ifPresent(ctx -> {
+ ctx.scopeType = ScopeType.SERVICE_RELATION;
+ ctx.scopeLabels.addAll(sourceServiceKeys);
+ ctx.scopeLabels.addAll(destServiceKeys);
+ ctx.scopeLabels.add(componentIdKey);
+ });
+ if (this == EMPTY) {
+ return EMPTY;
+ }
+ return createMeterSamples(new ServiceRelationEntityDescription(sourceServiceKeys, destServiceKeys, detectPoint, layer, delimiter, componentIdKey));
+ }
+
+ /**
+ * ForEach iteration using a Java functional interface.
+ * Replaces upstream forEach(List, Closure) — no Groovy Closure needed.
+ */
+ public SampleFamily forEach(List<String> array, ForEachFunction each) {
+ if (this == EMPTY) {
+ return EMPTY;
+ }
+ return SampleFamily.build(this.context, Arrays.stream(this.samples).map(sample -> {
+ Map<String, String> labels = Maps.newHashMap(sample.getLabels());
+ for (String element : array) {
+ each.accept(element, labels);
+ }
+ return sample.toBuilder().labels(ImmutableMap.copyOf(labels)).build();
+ }).toArray(Sample[]::new));
+ }
+
+ public SampleFamily processRelation(String detectPointKey, List<String> serviceKeys, List<String> instanceKeys, String sourceProcessIdKey, String destProcessIdKey, String componentKey) {
+ Preconditions.checkArgument(serviceKeys.size() > 0);
+ Preconditions.checkArgument(instanceKeys.size() > 0);
+ Preconditions.checkArgument(StringUtil.isNotEmpty(sourceProcessIdKey));
+ Preconditions.checkArgument(StringUtil.isNotEmpty(destProcessIdKey));
+ ExpressionParsingContext.get().ifPresent(ctx -> {
+ ctx.scopeType = ScopeType.PROCESS_RELATION;
+ ctx.scopeLabels.addAll(serviceKeys);
+ ctx.scopeLabels.addAll(instanceKeys);
+ ctx.scopeLabels.add(detectPointKey);
+ ctx.scopeLabels.add(sourceProcessIdKey);
+ ctx.scopeLabels.add(destProcessIdKey);
+ ctx.scopeLabels.add(componentKey);
+ });
+ if (this == EMPTY) {
+ return EMPTY;
+ }
+ return createMeterSamples(new ProcessRelationEntityDescription(serviceKeys, instanceKeys, sourceProcessIdKey, destProcessIdKey, detectPointKey, componentKey, Const.POINT));
+ }
+
+ private SampleFamily createMeterSamples(EntityDescription entityDescription) {
+ Map<MeterEntity, Sample[]> meterSamples = new HashMap<>();
+ Arrays.stream(samples)
+ .collect(groupingBy(it -> InternalOps.getLabels(entityDescription.getLabelKeys(), it),
+ mapping(identity(), toList())
+ ))
+ .forEach((labels, samples) -> {
+ MeterEntity meterEntity = InternalOps.buildMeterEntity(samples, entityDescription);
+ meterSamples.put(
+ meterEntity, InternalOps.left(samples, entityDescription.getLabelKeys()));
+ });
+
+ this.context.setMeterSamples(meterSamples);
+ return SampleFamily.build(this.context, samples);
+ }
+
+ private SampleFamily match(String[] labels, Function2<String, String, Boolean> op) {
+ Preconditions.checkArgument(labels.length % 2 == 0);
+ Map<String, String> ll = new HashMap<>(labels.length / 2);
+ for (int i = 0; i < labels.length; i += 2) {
+ ll.put(labels[i], labels[i + 1]);
+ }
+ Sample[] ss = Arrays.stream(samples)
+ .filter(sample -> ll.entrySet()
+ .stream()
+ .allMatch(
+ entry -> op.apply(sample.labels.getOrDefault(entry.getKey(), ""),
+ entry.getValue()
+ )))
+ .toArray(Sample[]::new);
+ return ss.length > 0 ? SampleFamily.build(this.context, ss) : EMPTY;
+ }
+
+ private SampleFamily valueMatch(CompType compType,
+ double compValue,
+ Function3<CompType, Double, Double, Boolean> op) {
+ Sample[] ss = Arrays.stream(samples)
+ .filter(sample -> op.apply(compType, sample.value, compValue)).toArray(Sample[]::new);
+ return ss.length > 0 ? SampleFamily.build(this.context, ss) : EMPTY;
+ }
+
+ public SampleFamily newValue(Function<Double, Double> transform) {
+ if (this == EMPTY) {
+ return EMPTY;
+ }
+ Sample[] ss = new Sample[samples.length];
+ for (int i = 0; i < ss.length; i++) {
+ ss[i] = samples[i].newValue(transform);
+ }
+ return SampleFamily.build(this.context, ss);
+ }
+
+ private SampleFamily newValue(SampleFamily another, Function2<Double, Double, Double> transform) {
+ Sample[] ss = Arrays.stream(samples)
+ .flatMap(cs -> io.vavr.collection.Stream.of(another.samples)
+ .find(as -> cs.labels.equals(as.labels))
+ .map(as -> cs.toBuilder()
+ .value(transform.apply(cs.value,
+ as.value
+ )))
+ .map(Sample.SampleBuilder::build)
+ .toJavaStream())
+ .toArray(Sample[]::new);
+ return ss.length > 0 ? SampleFamily.build(this.context, ss) : EMPTY;
+ }
+
+ public SampleFamily downsampling(final DownsamplingType type) {
+ ExpressionParsingContext.get().ifPresent(it -> it.downsampling = type);
+ return this;
+ }
+
+ /**
+ * Decorate the service meter entity using a Java functional interface.
+ * Replaces upstream decorate(Closure) — no Groovy Closure needed.
+ */
+ public SampleFamily decorate(DecorateFunction fn) {
+ ExpressionParsingContext.get().ifPresent(ctx -> {
+ if (ctx.getScopeType() != ScopeType.SERVICE) {
+ throw new IllegalStateException("decorate() should be invoked after service()");
+ }
+ if (ctx.isHistogram()) {
+ throw new IllegalStateException("decorate() not supported for histogram metrics");
+ }
+ });
+ if (this == EMPTY) {
+ return EMPTY;
+ }
+ this.context.getMeterSamples().keySet().forEach(meterEntity -> {
+ if (meterEntity.getScopeType().equals(ScopeType.SERVICE)) {
+ fn.accept(meterEntity);
+ }
+ });
+ return this;
+ }
+
+ @ToString
+ @EqualsAndHashCode(exclude = "metricName")
+ @Getter
+ @Setter
+ @Builder
+ public static class RunningContext {
+
+ static RunningContext EMPTY = instance();
+
+ static RunningContext instance() {
+ return RunningContext.builder()
+ .defaultHistogramBucketUnit(TimeUnit.SECONDS)
+ .build();
+ }
+
+ private String metricName;
+
+ @Builder.Default
+ private Map<MeterEntity, Sample[]> meterSamples = new HashMap<>();
+
+ private TimeUnit defaultHistogramBucketUnit;
+ }
+
+ private static class InternalOps {
+
+ private static Sample[] left(List<Sample> samples, List<String> labelKeys) {
+ return samples.stream().map(s -> {
+ ImmutableMap<String, String> ll = ImmutableMap.<String, String>builder()
+ .putAll(Maps.filterKeys(s.labels,
+ key -> !labelKeys.contains(key)
+ ))
+ .build();
+ return s.toBuilder().labels(ll).build();
+ }).toArray(Sample[]::new);
+ }
+
+ private static String dim(List<Sample> samples, List<String> labelKeys, String delimiter) {
+ String name = labelKeys.stream()
+ .map(k -> samples.get(0).labels.getOrDefault(k, ""))
+ .filter(v -> !StringUtil.isEmpty(v))
+ .collect(Collectors.joining(StringUtil.isEmpty(delimiter) ? Const.POINT : delimiter));
+ return name;
+ }
+
+ private static MeterEntity buildMeterEntity(List<Sample> samples,
+ EntityDescription entityDescription) {
+ switch (entityDescription.getScopeType()) {
+ case SERVICE:
+ ServiceEntityDescription serviceEntityDescription = (ServiceEntityDescription) entityDescription;
+ return MeterEntity.newService(
+ InternalOps.dim(samples, serviceEntityDescription.getServiceKeys(), serviceEntityDescription.getDelimiter()),
+ serviceEntityDescription.getLayer()
+ );
+ case SERVICE_INSTANCE:
+ InstanceEntityDescription instanceEntityDescription = (InstanceEntityDescription) entityDescription;
+ Map<String, String> properties = null;
+ if (instanceEntityDescription.getPropertiesExtractor() != null) {
+ properties = instanceEntityDescription.getPropertiesExtractor().apply(samples.get(0).labels);
+ }
+ return MeterEntity.newServiceInstance(
+ InternalOps.dim(samples, instanceEntityDescription.getServiceKeys(), instanceEntityDescription.getServiceDelimiter()),
+ InternalOps.dim(samples, instanceEntityDescription.getInstanceKeys(), instanceEntityDescription.getInstanceDelimiter()),
+ instanceEntityDescription.getLayer(),
+ properties
+ );
+ case ENDPOINT:
+ EndpointEntityDescription endpointEntityDescription = (EndpointEntityDescription) entityDescription;
+ return MeterEntity.newEndpoint(
+ InternalOps.dim(samples, endpointEntityDescription.getServiceKeys(), endpointEntityDescription.getDelimiter()),
+ InternalOps.dim(samples, endpointEntityDescription.getEndpointKeys(), endpointEntityDescription.getDelimiter()),
+ endpointEntityDescription.getLayer()
+ );
+ case PROCESS:
+ final ProcessEntityDescription processEntityDescription = (ProcessEntityDescription) entityDescription;
+ return MeterEntity.newProcess(
+ InternalOps.dim(samples, processEntityDescription.getServiceKeys(), processEntityDescription.getDelimiter()),
+ InternalOps.dim(samples, processEntityDescription.getServiceInstanceKeys(), processEntityDescription.getDelimiter()),
+ InternalOps.dim(samples, processEntityDescription.getProcessKeys(), processEntityDescription.getDelimiter()),
+ InternalOps.dim(samples, List.of(processEntityDescription.getLayerKey()), processEntityDescription.getDelimiter())
+ );
+ case SERVICE_RELATION:
+ ServiceRelationEntityDescription serviceRelationEntityDescription = (ServiceRelationEntityDescription) entityDescription;
+ final String serviceRelationComponentValue = InternalOps.dim(samples,
+ Collections.singletonList(serviceRelationEntityDescription.getComponentIdKey()), serviceRelationEntityDescription.getDelimiter());
+ int serviceRelationComponentId = StringUtil.isNotEmpty(serviceRelationComponentValue) ? Integer.parseInt(serviceRelationComponentValue) : 0;
+ return MeterEntity.newServiceRelation(
+ InternalOps.dim(samples, serviceRelationEntityDescription.getSourceServiceKeys(), serviceRelationEntityDescription.getDelimiter()),
+ InternalOps.dim(samples, serviceRelationEntityDescription.getDestServiceKeys(), serviceRelationEntityDescription.getDelimiter()),
+ serviceRelationEntityDescription.getDetectPoint(), serviceRelationEntityDescription.getLayer(), serviceRelationComponentId
+ );
+ case PROCESS_RELATION:
+ final ProcessRelationEntityDescription processRelationEntityDescription = (ProcessRelationEntityDescription) entityDescription;
+ final String detectPointValue = InternalOps.dim(samples, Collections.singletonList(processRelationEntityDescription.getDetectPointKey()), processRelationEntityDescription.getDelimiter());
+ DetectPoint point = StringUtils.equalsAnyIgnoreCase(detectPointValue, "server") ? DetectPoint.SERVER : DetectPoint.CLIENT;
+ final String componentValue = InternalOps.dim(samples, Collections.singletonList(processRelationEntityDescription.getComponentKey()), processRelationEntityDescription.getDelimiter());
+ final int componentId = StringUtil.isNotEmpty(componentValue) ? Integer.parseInt(componentValue) : 0;
+ return MeterEntity.newProcessRelation(
+ InternalOps.dim(samples, processRelationEntityDescription.getServiceKeys(), processRelationEntityDescription.getDelimiter()),
+ InternalOps.dim(samples, processRelationEntityDescription.getInstanceKeys(), processRelationEntityDescription.getDelimiter()),
+ InternalOps.dim(samples, Collections.singletonList(processRelationEntityDescription.getSourceProcessIdKey()), processRelationEntityDescription.getDelimiter()),
+ InternalOps.dim(samples, Collections.singletonList(processRelationEntityDescription.getDestProcessIdKey()), processRelationEntityDescription.getDelimiter()),
+ componentId,
+ point
+ );
+ default:
+ throw new UnexpectedException(
+ "Unexpected scope type of entityDescription " + entityDescription);
+ }
+ }
+
+ private static Sample newSample(String name,
+ ImmutableMap<String, String> labels,
+ long timestamp,
+ double newValue) {
+ return Sample.builder()
+ .value(newValue)
+ .labels(labels)
+ .timestamp(timestamp)
+ .name(name)
+ .build();
+ }
+
+ private static boolean stringComp(String a, String b) {
+ if (Strings.isNullOrEmpty(a) && Strings.isNullOrEmpty(b)) {
+ return true;
+ }
+ if (Strings.isNullOrEmpty(a)) {
+ return false;
+ }
+ return a.equals(b);
+ }
+
+ private static boolean doubleComp(CompType compType, double a, double b) {
+ int result = Double.compare(a, b);
+ switch (compType) {
+ case EQUAL:
+ return result == 0;
+ case NOT_EQUAL:
+ return result != 0;
+ case GREATER:
+ return result == 1;
+ case GREATER_EQUAL:
+ return result == 0 || result == 1;
+ case LESS:
+ return result == -1;
+ case LESS_EQUAL:
+ return result == 0 || result == -1;
+ }
+
+ return false;
+ }
+
+ private static ImmutableMap<String, String> getLabels(final List<String> labelKeys, final Sample sample) {
+ return labelKeys.stream()
+ .collect(toImmutableMap(
+ Function.identity(),
+ labelKey -> sample.labels.getOrDefault(labelKey, "")
+ ));
+ }
+
+ private static ImmutableMap<String, String> groupByExcludedLabel(final String excludedLabelKey, final Sample sample) {
+ return sample
+ .labels
+ .entrySet()
+ .stream()
+ .filter(v -> !v.getKey().equals(excludedLabelKey))
+ .collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
+ }
+ }
+
+ private enum CompType {
+ EQUAL, NOT_EQUAL, LESS, LESS_EQUAL, GREATER, GREATER_EQUAL
+ }
+}
diff --git a/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/SampleFamilyFunctions.java b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/SampleFamilyFunctions.java
new file mode 100644
index 0000000..41bc728
--- /dev/null
+++ b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/SampleFamilyFunctions.java
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+
+package org.apache.skywalking.oap.meter.analyzer.dsl;
+
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import org.apache.skywalking.oap.server.core.analysis.meter.MeterEntity;
+
+/**
+ * Java functional interfaces replacing Groovy Closure parameters in SampleFamily methods.
+ * These are used by the generated MAL expression classes (transpiled from Groovy DSL).
+ */
+public final class SampleFamilyFunctions {
+
+ private SampleFamilyFunctions() {
+ }
+
+ /**
+ * Tag transformation function: receives mutable tag map, modifies it, returns modified map.
+ * Replaces Closure<?> in SampleFamily.tag().
+ */
+ @FunctionalInterface
+ public interface TagFunction extends Function<Map<String, String>, Map<String, String>> {
+ }
+
+ /**
+ * Sample filter predicate: receives tag map, returns true to keep the sample.
+ * Replaces Closure<Boolean> in SampleFamily.filter().
+ */
+ @FunctionalInterface
+ public interface SampleFilter extends Predicate<Map<String, String>> {
+ }
+
+ /**
+ * ForEach function: receives an array element and mutable label map.
+ * Replaces Closure<Void> in SampleFamily.forEach().
+ */
+ @FunctionalInterface
+ public interface ForEachFunction extends BiConsumer<String, Map<String, String>> {
+ }
+
+ /**
+ * Decorate function: receives a MeterEntity to modify.
+ * Replaces Closure<Void> in SampleFamily.decorate().
+ */
+ @FunctionalInterface
+ public interface DecorateFunction extends Consumer<MeterEntity> {
+ }
+
+ /**
+ * Properties extractor: receives label map, returns properties map.
+ * Replaces Closure<Map> in SampleFamily.instance() overload.
+ */
+ @FunctionalInterface
+ public interface PropertiesExtractor extends Function<Map<String, String>, Map<String, String>> {
+ }
+}