This is an automated email from the ASF dual-hosted git repository. wusheng pushed a commit to branch groovy-replace in repository https://gitbox.apache.org/repos/asf/skywalking.git
commit a22f67a7918a711b0bf38a66775bdb9689168568 Author: Wu Sheng <[email protected]> AuthorDate: Sat Feb 28 15:42:37 2026 +0800 Add LAL transpiler module for build-time Groovy-to-Java conversion (Phase 3) Introduces lal-transpiler module that parses LAL Groovy DSL scripts into AST at Phases.CONVERSION and emits pure Java classes implementing LalExpression. Handles filter/text/json/yaml/extractor/sink/abort blocks, parsed property access, safe navigation, cast expressions, GString interpolation, and SHA-256 deduplication. Makes MalToJavaTranspiler.escapeJava() public for cross-module reuse. Includes 37 comprehensive tests. Co-Authored-By: Claude Opus 4.6 <[email protected]> --- oap-server/analyzer/{ => lal-transpiler}/pom.xml | 22 +- .../server/transpiler/lal/LalToJavaTranspiler.java | 923 +++++++++++++++++++++ .../transpiler/lal/LalToJavaTranspilerTest.java | 678 +++++++++++++++ .../server/transpiler/mal/MalToJavaTranspiler.java | 2 +- oap-server/analyzer/pom.xml | 1 + 5 files changed, 1609 insertions(+), 17 deletions(-) diff --git a/oap-server/analyzer/pom.xml b/oap-server/analyzer/lal-transpiler/pom.xml similarity index 73% copy from oap-server/analyzer/pom.xml copy to oap-server/analyzer/lal-transpiler/pom.xml index 4039928a50..b722b255fc 100644 --- a/oap-server/analyzer/pom.xml +++ b/oap-server/analyzer/lal-transpiler/pom.xml @@ -19,38 +19,28 @@ <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> - <artifactId>oap-server</artifactId> + <artifactId>analyzer</artifactId> <groupId>org.apache.skywalking</groupId> <version>${revision}</version> </parent> <modelVersion>4.0.0</modelVersion> - <artifactId>analyzer</artifactId> - <packaging>pom</packaging> - - <modules> - <module>agent-analyzer</module> - <module>log-analyzer</module> - <module>meter-analyzer</module> - <module>event-analyzer</module> - <module>mal-transpiler</module> - </modules> + <artifactId>lal-transpiler</artifactId> <dependencies> <dependency> <groupId>org.apache.skywalking</groupId> - <artifactId>apm-network</artifactId> + <artifactId>log-analyzer</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.apache.skywalking</groupId> - <artifactId>library-module</artifactId> + <artifactId>mal-transpiler</artifactId> <version>${project.version}</version> </dependency> <dependency> - <groupId>org.apache.skywalking</groupId> - <artifactId>library-util</artifactId> - <version>${project.version}</version> + <groupId>org.apache.groovy</groupId> + <artifactId>groovy</artifactId> </dependency> </dependencies> </project> diff --git a/oap-server/analyzer/lal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/lal/LalToJavaTranspiler.java b/oap-server/analyzer/lal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/lal/LalToJavaTranspiler.java new file mode 100644 index 0000000000..ae7ad63f08 --- /dev/null +++ b/oap-server/analyzer/lal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/lal/LalToJavaTranspiler.java @@ -0,0 +1,923 @@ +/* + * 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.transpiler.lal; + +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.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.apache.skywalking.oap.server.transpiler.mal.MalToJavaTranspiler; +import org.codehaus.groovy.ast.ModuleNode; +import org.codehaus.groovy.ast.expr.ArgumentListExpression; +import org.codehaus.groovy.ast.expr.BinaryExpression; +import org.codehaus.groovy.ast.expr.BooleanExpression; +import org.codehaus.groovy.ast.expr.CastExpression; +import org.codehaus.groovy.ast.expr.ClosureExpression; +import org.codehaus.groovy.ast.expr.ConstantExpression; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.expr.GStringExpression; +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.NotExpression; +import org.codehaus.groovy.ast.expr.PropertyExpression; +import org.codehaus.groovy.ast.expr.TupleExpression; +import org.codehaus.groovy.ast.expr.VariableExpression; +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.Statement; +import org.codehaus.groovy.control.CompilationUnit; +import org.codehaus.groovy.control.CompilerConfiguration; +import org.codehaus.groovy.control.Phases; +import org.codehaus.groovy.syntax.Types; + +/** + * Transpiles LAL (Log Analysis Language) Groovy scripts to Java source code at build time. + * Parses DSL strings into Groovy AST (via CompilationUnit at CONVERSION phase), + * walks AST nodes, and produces Java classes implementing LalExpression. + */ +@Slf4j +public class LalToJavaTranspiler { + + static final String GENERATED_PACKAGE = + "org.apache.skywalking.oap.server.core.source.oal.rt.lal"; + + private static final Set<String> CONSUMER_METHODS = Set.of( + "text", "extractor", "sink", "slowSql", "sampledTrace", "metrics", "sampler" + ); + + private static final Set<String> STATIC_TYPES = Set.of("ProcessRegistry"); + + // ---- Batch state ---- + + private final Map<String, String> lalSources = new LinkedHashMap<>(); + private final Map<String, String> hashToClass = new LinkedHashMap<>(); + + private int tempVarCounter; + + /** + * Transpile a LAL DSL script to a Java class implementing LalExpression. + * + * @param className simple class name (e.g. "LalExpr_3") + * @param dslText the Groovy DSL string (filter { ... }) + * @return Java source code + */ + public String transpile(final String className, final String dslText) { + tempVarCounter = 0; + final ModuleNode module = parseToAST(dslText); + final Statement body = extractBody(module); + + final StringBuilder sb = new StringBuilder(); + emitHeader(sb, className); + + final List<Statement> stmts = flattenStatements(body); + for (Statement stmt : stmts) { + emitStatement(sb, stmt, "filterSpec", "binding", 2); + } + + emitFooter(sb); + return sb.toString(); + } + + public void register(final String className, final String hash, final String source) { + lalSources.put(className, source); + hashToClass.put(hash, GENERATED_PACKAGE + "." + className); + } + + // ---- AST Parsing ---- + + ModuleNode parseToAST(final String expression) { + final CompilerConfiguration cc = new CompilerConfiguration(); + final CompilationUnit cu = new CompilationUnit(cc); + cu.addSource("Script", expression); + cu.compile(Phases.CONVERSION); + final List<ModuleNode> modules = cu.getAST().getModules(); + if (modules.isEmpty()) { + throw new IllegalStateException("No AST modules produced"); + } + return modules.get(0); + } + + Statement extractBody(final ModuleNode module) { + final BlockStatement block = module.getStatementBlock(); + if (block != null && !block.getStatements().isEmpty()) { + return block; + } + throw new IllegalStateException("Empty AST body"); + } + + // ---- Code Generation ---- + + private void emitHeader(final StringBuilder sb, final String className) { + sb.append("package ").append(GENERATED_PACKAGE).append(";\n\n"); + sb.append("import java.util.Map;\n"); + sb.append("import org.apache.skywalking.oap.log.analyzer.dsl.Binding;\n"); + sb.append("import org.apache.skywalking.oap.log.analyzer.dsl.LalExpression;\n"); + sb.append("import org.apache.skywalking.oap.log.analyzer.dsl.spec.filter.FilterSpec;\n\n"); + sb.append("@SuppressWarnings(\"unchecked\")\n"); + sb.append("public class ").append(className).append(" implements LalExpression {\n\n"); + emitHelpers(sb); + sb.append(" @Override\n"); + sb.append(" public void execute(FilterSpec filterSpec, Binding binding) {\n"); + } + + private void emitHelpers(final StringBuilder sb) { + sb.append(" private static Object getAt(Object obj, String key) {\n"); + sb.append(" if (obj == null) return null;\n"); + sb.append(" if (obj instanceof Binding.Parsed) return ((Binding.Parsed) obj).getAt(key);\n"); + sb.append(" if (obj instanceof Map) return ((Map<String, Object>) obj).get(key);\n"); + sb.append(" return null;\n"); + sb.append(" }\n\n"); + + sb.append(" private static long toLong(Object obj) {\n"); + sb.append(" if (obj instanceof Number) return ((Number) obj).longValue();\n"); + sb.append(" if (obj instanceof String) return Long.parseLong((String) obj);\n"); + sb.append(" return 0L;\n"); + sb.append(" }\n\n"); + + sb.append(" private static int toInt(Object obj) {\n"); + sb.append(" if (obj instanceof Number) return ((Number) obj).intValue();\n"); + sb.append(" if (obj instanceof String) return Integer.parseInt((String) obj);\n"); + sb.append(" return 0;\n"); + sb.append(" }\n\n"); + + sb.append(" private static boolean toBoolean(Object obj) {\n"); + sb.append(" if (obj instanceof Boolean) return (Boolean) obj;\n"); + sb.append(" if (obj instanceof String) return Boolean.parseBoolean((String) obj);\n"); + sb.append(" return obj != null;\n"); + sb.append(" }\n\n"); + + sb.append(" private static boolean isTruthy(Object obj) {\n"); + sb.append(" if (obj == null) return false;\n"); + sb.append(" if (obj instanceof Boolean) return (Boolean) obj;\n"); + sb.append(" if (obj instanceof String) return !((String) obj).isEmpty();\n"); + sb.append(" if (obj instanceof Number) return ((Number) obj).doubleValue() != 0;\n"); + sb.append(" return true;\n"); + sb.append(" }\n\n"); + + sb.append(" private static boolean isNonEmptyString(Object obj) {\n"); + sb.append(" if (obj == null) return false;\n"); + sb.append(" String s = obj.toString();\n"); + sb.append(" return s != null && !s.trim().isEmpty();\n"); + sb.append(" }\n\n"); + } + + private void emitFooter(final StringBuilder sb) { + sb.append(" }\n"); + sb.append("}\n"); + } + + private void emitStatement(final StringBuilder sb, final Statement stmt, + final String receiver, final String bindingVar, final int indent) { + if (stmt instanceof BlockStatement) { + for (Statement s : ((BlockStatement) stmt).getStatements()) { + emitStatement(sb, s, receiver, bindingVar, indent); + } + } else if (stmt instanceof ExpressionStatement) { + emitExpressionStatement(sb, ((ExpressionStatement) stmt).getExpression(), + receiver, bindingVar, indent); + } else if (stmt instanceof IfStatement) { + emitIfStatement(sb, (IfStatement) stmt, receiver, bindingVar, indent); + } else if (stmt instanceof EmptyStatement) { + // skip + } else { + throw new UnsupportedOperationException( + "Unsupported statement: " + stmt.getClass().getSimpleName()); + } + } + + private void emitExpressionStatement(final StringBuilder sb, final Expression expr, + final String receiver, final String bindingVar, + final int indent) { + if (expr instanceof MethodCallExpression) { + emitMethodCall(sb, (MethodCallExpression) expr, receiver, bindingVar, indent); + } else if (expr instanceof BinaryExpression) { + emitBinaryAsNamedArg(sb, (BinaryExpression) expr, receiver, bindingVar, indent); + } else { + throw new UnsupportedOperationException( + "Unsupported expression statement: " + expr.getClass().getSimpleName() + + " (" + expr.getText() + ")"); + } + } + + private void emitMethodCall(final StringBuilder sb, final MethodCallExpression mce, + final String receiver, final String bindingVar, final int indent) { + final String methodName = mce.getMethodAsString(); + final Expression objExpr = mce.getObjectExpression(); + final ArgumentListExpression args = toArgList(mce.getArguments()); + final List<Expression> argExprs = args.getExpressions(); + + // Top-level filter { ... } -> unwrap and emit body + if ("filter".equals(methodName) && isThisOrImplicit(objExpr)) { + if (!argExprs.isEmpty() && argExprs.get(0) instanceof ClosureExpression) { + final ClosureExpression closure = (ClosureExpression) argExprs.get(0); + emitStatement(sb, closure.getCode(), receiver, bindingVar, indent); + return; + } + } + + // json {} -> filterSpec.json() + if ("json".equals(methodName) && isThisOrImplicit(objExpr)) { + indent(sb, indent); + sb.append(receiver).append(".json();\n"); + return; + } + + // text { regexp /pattern/ } -> filterSpec.text(tp -> tp.regexp("pattern")) + if ("text".equals(methodName) && isThisOrImplicit(objExpr) + && !argExprs.isEmpty() && argExprs.get(0) instanceof ClosureExpression) { + final ClosureExpression closure = (ClosureExpression) argExprs.get(0); + final String tpVar = "tp"; + indent(sb, indent); + sb.append(receiver).append(".text(").append(tpVar).append(" -> {\n"); + emitStatement(sb, closure.getCode(), tpVar, bindingVar, indent + 1); + indent(sb, indent); + sb.append("});\n"); + return; + } + + // regexp /pattern/ -> tp.regexp("pattern") + if ("regexp".equals(methodName) && isThisOrImplicit(objExpr)) { + indent(sb, indent); + sb.append(receiver).append(".regexp("); + if (!argExprs.isEmpty()) { + sb.append(visitValueExpression(argExprs.get(0), bindingVar)); + } + sb.append(");\n"); + return; + } + + // Consumer overload methods: extractor, sink, slowSql, sampledTrace, metrics, sampler + if (CONSUMER_METHODS.contains(methodName) && isThisOrImplicit(objExpr) + && !argExprs.isEmpty() && argExprs.get(0) instanceof ClosureExpression) { + final ClosureExpression closure = (ClosureExpression) argExprs.get(0); + final String lambdaVar = lambdaVarFor(methodName); + final String childReceiver = childReceiverFor(lambdaVar); + indent(sb, indent); + sb.append(receiver).append(".").append(methodName).append("(") + .append(lambdaVar).append(" -> {\n"); + emitStatement(sb, closure.getCode(), childReceiver, bindingVar, indent + 1); + indent(sb, indent); + sb.append("});\n"); + return; + } + + // rateLimit("${expr}") { rpm N } -> sp.rateLimit(idExpr, rls -> rls.rpm(N)) + if ("rateLimit".equals(methodName) && isThisOrImplicit(objExpr)) { + final Expression idArg = argExprs.get(0); + final ClosureExpression closure = argExprs.size() > 1 + && argExprs.get(1) instanceof ClosureExpression + ? (ClosureExpression) argExprs.get(1) : null; + + indent(sb, indent); + sb.append(receiver).append(".rateLimit(") + .append(visitValueExpression(idArg, bindingVar)); + if (closure != null) { + final String rlsVar = "rls"; + sb.append(", ").append(rlsVar).append(" -> {\n"); + emitStatement(sb, closure.getCode(), rlsVar, bindingVar, indent + 1); + indent(sb, indent); + sb.append("}"); + } + sb.append(");\n"); + return; + } + + // abort {} -> filterSpec.abort() + if ("abort".equals(methodName) && isThisOrImplicit(objExpr)) { + indent(sb, indent); + sb.append(receiver).append(".abort();\n"); + return; + } + + // enforcer {} or dropper {} -> sink.enforcer() / sink.dropper() + if (("enforcer".equals(methodName) || "dropper".equals(methodName)) + && isThisOrImplicit(objExpr)) { + indent(sb, indent); + sb.append(receiver).append(".").append(methodName).append("();\n"); + return; + } + + // tag("KEY") as standalone statement + if ("tag".equals(methodName) && isThisOrImplicit(objExpr) + && !argExprs.isEmpty() && argExprs.get(0) instanceof ConstantExpression) { + indent(sb, indent); + sb.append(receiver).append(".tag(\"") + .append(MalToJavaTranspiler.escapeJava(argExprs.get(0).getText())) + .append("\");\n"); + return; + } + + // Simple value-setting methods: service(val), layer(val), timestamp(val), etc. + if (isThisOrImplicit(objExpr)) { + indent(sb, indent); + sb.append(receiver).append(".").append(methodName).append("("); + for (int i = 0; i < argExprs.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(visitValueExpression(argExprs.get(i), bindingVar)); + } + sb.append(");\n"); + return; + } + + // Static method: ProcessRegistry.generateVirtualLocalProcess(...) + if (objExpr instanceof VariableExpression + && STATIC_TYPES.contains(((VariableExpression) objExpr).getName())) { + indent(sb, indent); + sb.append("org.apache.skywalking.oap.meter.analyzer.dsl.registry.ProcessRegistry.") + .append(methodName).append("("); + for (int i = 0; i < argExprs.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(visitValueExpression(argExprs.get(i), bindingVar)); + } + sb.append(");\n"); + return; + } + + throw new UnsupportedOperationException( + "Unsupported method call: " + methodName + " on " + objExpr.getClass().getSimpleName() + + " (" + mce.getText() + ")"); + } + + private void emitBinaryAsNamedArg(final StringBuilder sb, final BinaryExpression expr, + final String receiver, final String bindingVar, + final int indent) { + throw new UnsupportedOperationException( + "Unsupported binary expression as statement: " + expr.getText()); + } + + // ---- If/Else ---- + + private void emitIfStatement(final StringBuilder sb, final IfStatement ifStmt, + final String receiver, final String bindingVar, final int indent) { + indent(sb, indent); + sb.append("if ("); + sb.append(visitCondition(ifStmt.getBooleanExpression(), bindingVar)); + sb.append(") {\n"); + + emitStatement(sb, ifStmt.getIfBlock(), receiver, bindingVar, indent + 1); + + final Statement elseBlock = ifStmt.getElseBlock(); + if (elseBlock != null && !(elseBlock instanceof EmptyStatement)) { + indent(sb, indent); + if (elseBlock instanceof IfStatement) { + sb.append("} else "); + emitIfStatementInline(sb, (IfStatement) elseBlock, receiver, bindingVar, indent); + return; + } else { + sb.append("} else {\n"); + emitStatement(sb, elseBlock, receiver, bindingVar, indent + 1); + indent(sb, indent); + sb.append("}\n"); + } + } else { + indent(sb, indent); + sb.append("}\n"); + } + } + + private void emitIfStatementInline(final StringBuilder sb, final IfStatement ifStmt, + final String receiver, final String bindingVar, + final int indent) { + sb.append("if ("); + sb.append(visitCondition(ifStmt.getBooleanExpression(), bindingVar)); + sb.append(") {\n"); + + emitStatement(sb, ifStmt.getIfBlock(), receiver, bindingVar, indent + 1); + + final Statement elseBlock = ifStmt.getElseBlock(); + if (elseBlock != null && !(elseBlock instanceof EmptyStatement)) { + indent(sb, indent); + if (elseBlock instanceof IfStatement) { + sb.append("} else "); + emitIfStatementInline(sb, (IfStatement) elseBlock, receiver, bindingVar, indent); + } else { + sb.append("} else {\n"); + emitStatement(sb, elseBlock, receiver, bindingVar, indent + 1); + indent(sb, indent); + sb.append("}\n"); + } + } else { + indent(sb, indent); + sb.append("}\n"); + } + } + + // ---- Condition Visiting ---- + + private String visitCondition(final BooleanExpression boolExpr, final String bindingVar) { + return visitConditionExpr(boolExpr.getExpression(), bindingVar); + } + + String visitConditionExpr(final Expression expr, final String bindingVar) { + if (expr instanceof BinaryExpression) { + final BinaryExpression bin = (BinaryExpression) expr; + final int op = bin.getOperation().getType(); + + if (op == Types.COMPARE_EQUAL) { + final String left = visitValueExpression(bin.getLeftExpression(), bindingVar); + final String right = visitValueExpression(bin.getRightExpression(), bindingVar); + if (bin.getRightExpression() instanceof ConstantExpression + && ((ConstantExpression) bin.getRightExpression()).getValue() instanceof String) { + return "\"" + MalToJavaTranspiler.escapeJava( + (String) ((ConstantExpression) bin.getRightExpression()).getValue()) + + "\".equals(" + left + ")"; + } + return "java.util.Objects.equals(" + left + ", " + right + ")"; + } + + if (op == Types.COMPARE_NOT_EQUAL) { + final String left = visitValueExpression(bin.getLeftExpression(), bindingVar); + final String right = visitValueExpression(bin.getRightExpression(), bindingVar); + if (bin.getRightExpression() instanceof ConstantExpression + && ((ConstantExpression) bin.getRightExpression()).getValue() instanceof String) { + return "!\"" + MalToJavaTranspiler.escapeJava( + (String) ((ConstantExpression) bin.getRightExpression()).getValue()) + + "\".equals(" + left + ")"; + } + return "!java.util.Objects.equals(" + left + ", " + right + ")"; + } + + if (op == Types.COMPARE_LESS_THAN) { + final String left = visitValueExpression(bin.getLeftExpression(), bindingVar); + final String right = visitValueExpression(bin.getRightExpression(), bindingVar); + return "toInt(" + left + ") < " + right; + } + + if (op == Types.COMPARE_GREATER_THAN_EQUAL) { + final String left = visitValueExpression(bin.getLeftExpression(), bindingVar); + final String right = visitValueExpression(bin.getRightExpression(), bindingVar); + return "toInt(" + left + ") >= " + right; + } + + if (op == Types.LOGICAL_AND) { + return visitConditionExpr(bin.getLeftExpression(), bindingVar) + + " && " + visitConditionExpr(bin.getRightExpression(), bindingVar); + } + + if (op == Types.LOGICAL_OR) { + return visitConditionExpr(bin.getLeftExpression(), bindingVar) + + " || " + visitConditionExpr(bin.getRightExpression(), bindingVar); + } + + throw new UnsupportedOperationException( + "Unsupported condition operator: " + bin.getOperation().getText()); + } + + if (expr instanceof NotExpression) { + final String inner = visitConditionExpr(((NotExpression) expr).getExpression(), bindingVar); + return "!" + inner; + } + + if (expr instanceof BooleanExpression) { + return visitConditionExpr(((BooleanExpression) expr).getExpression(), bindingVar); + } + + if (expr instanceof MethodCallExpression) { + final MethodCallExpression mce = (MethodCallExpression) expr; + final String methodName = mce.getMethodAsString(); + if ("toString".equals(methodName) || "trim".equals(methodName)) { + final String obj = visitValueExpression(mce.getObjectExpression(), bindingVar); + return "isNonEmptyString(" + obj + ")"; + } + return visitValueExpression(expr, bindingVar); + } + + return "isTruthy(" + visitValueExpression(expr, bindingVar) + ")"; + } + + // ---- Value Expression Visiting ---- + + String visitValueExpression(final Expression expr, final String bindingVar) { + if (expr instanceof ConstantExpression) { + return visitConstant((ConstantExpression) expr); + } + + if (expr instanceof VariableExpression) { + return visitVariable((VariableExpression) expr, bindingVar); + } + + if (expr instanceof PropertyExpression) { + return visitProperty((PropertyExpression) expr, bindingVar); + } + + if (expr instanceof CastExpression) { + return visitCast((CastExpression) expr, bindingVar); + } + + if (expr instanceof MethodCallExpression) { + return visitMethodCallValue((MethodCallExpression) expr, bindingVar); + } + + if (expr instanceof GStringExpression) { + return visitGString((GStringExpression) expr, bindingVar); + } + + if (expr instanceof MapExpression) { + return visitMapExpression((MapExpression) expr, bindingVar); + } + + if (expr instanceof BinaryExpression) { + final BinaryExpression bin = (BinaryExpression) expr; + final int op = bin.getOperation().getType(); + if (op == Types.COMPARE_EQUAL || op == Types.COMPARE_NOT_EQUAL + || op == Types.LOGICAL_AND || op == Types.LOGICAL_OR + || op == Types.COMPARE_LESS_THAN || op == Types.COMPARE_GREATER_THAN_EQUAL) { + return visitConditionExpr(expr, bindingVar); + } + } + + if (expr instanceof NotExpression) { + return "!" + visitValueExpression(((NotExpression) expr).getExpression(), bindingVar); + } + + throw new UnsupportedOperationException( + "Unsupported value expression: " + expr.getClass().getSimpleName() + + " (" + expr.getText() + ")"); + } + + private String visitConstant(final ConstantExpression expr) { + final Object value = expr.getValue(); + if (value instanceof String) { + return "\"" + MalToJavaTranspiler.escapeJava((String) value) + "\""; + } + if (value instanceof Integer) { + return value.toString(); + } + if (value instanceof Long) { + return value + "L"; + } + if (value instanceof Boolean) { + return value.toString(); + } + if (value instanceof Double) { + return value + "d"; + } + if (value == null) { + return "null"; + } + return value.toString(); + } + + private String visitVariable(final VariableExpression expr, final String bindingVar) { + final String name = expr.getName(); + if ("parsed".equals(name)) { + return bindingVar + ".parsed()"; + } + if ("log".equals(name)) { + return bindingVar + ".log()"; + } + if ("this".equals(name)) { + return "filterSpec"; + } + if (STATIC_TYPES.contains(name)) { + return "org.apache.skywalking.oap.meter.analyzer.dsl.registry.ProcessRegistry"; + } + return name; + } + + private String visitProperty(final PropertyExpression expr, final String bindingVar) { + final Expression objExpr = expr.getObjectExpression(); + final String propName = expr.getPropertyAsString(); + final boolean isSafe = expr.isSafe(); + + final String obj = visitValueExpression(objExpr, bindingVar); + + // log.service -> binding.log().getService() + if (objExpr instanceof VariableExpression + && "log".equals(((VariableExpression) objExpr).getName())) { + return visitLogProperty(propName, bindingVar); + } + + // For parsed access and nested map access, use getAt() + if (isSafe) { + return "(" + obj + " == null ? null : getAt(" + obj + ", \"" + propName + "\"))"; + } + + return "getAt(" + obj + ", \"" + propName + "\")"; + } + + private String visitLogProperty(final String propName, final String bindingVar) { + switch (propName) { + case "service": + return bindingVar + ".log().getService()"; + case "serviceInstance": + return bindingVar + ".log().getServiceInstance()"; + case "endpoint": + return bindingVar + ".log().getEndpoint()"; + case "timestamp": + return bindingVar + ".log().getTimestamp()"; + default: + return bindingVar + ".log().get" + capitalize(propName) + "()"; + } + } + + private String visitCast(final CastExpression expr, final String bindingVar) { + final String inner = visitValueExpression(expr.getExpression(), bindingVar); + final String typeName = expr.getType().getName(); + + switch (typeName) { + case "java.lang.String": + case "String": + return "String.valueOf(" + inner + ")"; + case "java.lang.Long": + case "Long": + case "long": + return "toLong(" + inner + ")"; + case "java.lang.Integer": + case "Integer": + case "int": + return "toInt(" + inner + ")"; + case "java.lang.Boolean": + case "Boolean": + case "boolean": + return "toBoolean(" + inner + ")"; + default: + return "((" + typeName + ") " + inner + ")"; + } + } + + private String visitMethodCallValue(final MethodCallExpression mce, final String bindingVar) { + final String methodName = mce.getMethodAsString(); + final Expression objExpr = mce.getObjectExpression(); + final ArgumentListExpression args = toArgList(mce.getArguments()); + final boolean isSafe = mce.isSafe(); + + // tag("KEY") on filterSpec -> filterSpec.tag("KEY") + if ("tag".equals(methodName) && isThisOrImplicit(objExpr)) { + final List<Expression> argExprs = args.getExpressions(); + if (!argExprs.isEmpty() && argExprs.get(0) instanceof ConstantExpression) { + return "filterSpec.tag(\"" + + MalToJavaTranspiler.escapeJava(argExprs.get(0).getText()) + "\")"; + } + } + + // ProcessRegistry.generateVirtualLocalProcess(...) + if (objExpr instanceof VariableExpression + && STATIC_TYPES.contains(((VariableExpression) objExpr).getName())) { + final StringBuilder sb = new StringBuilder(); + sb.append("org.apache.skywalking.oap.meter.analyzer.dsl.registry.ProcessRegistry."); + sb.append(methodName).append("("); + final List<Expression> argExprs = args.getExpressions(); + for (int i = 0; i < argExprs.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(visitValueExpression(argExprs.get(i), bindingVar)); + } + sb.append(")"); + return sb.toString(); + } + + // toString(), trim() on safe navigation chain + if ("toString".equals(methodName) || "trim".equals(methodName)) { + final String obj = visitValueExpression(objExpr, bindingVar); + if (isSafe) { + return "(" + obj + " == null ? null : " + obj + "." + methodName + "())"; + } + return obj + "." + methodName + "()"; + } + + // Generic method call + final String obj = visitValueExpression(objExpr, bindingVar); + final StringBuilder sb = new StringBuilder(); + if (isSafe) { + sb.append("(").append(obj).append(" == null ? null : "); + } + sb.append(obj).append(".").append(methodName).append("("); + final List<Expression> argExprs = args.getExpressions(); + for (int i = 0; i < argExprs.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(visitValueExpression(argExprs.get(i), bindingVar)); + } + sb.append(")"); + if (isSafe) { + sb.append(")"); + } + return sb.toString(); + } + + private String visitGString(final GStringExpression expr, final String bindingVar) { + final List<ConstantExpression> strings = expr.getStrings(); + final List<Expression> values = expr.getValues(); + + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < strings.size(); i++) { + final String text = strings.get(i).getText(); + if (!text.isEmpty()) { + if (sb.length() > 0) { + sb.append(" + "); + } + sb.append("\"").append(MalToJavaTranspiler.escapeJava(text)).append("\""); + } + if (i < values.size()) { + final String val = visitValueExpression(values.get(i), bindingVar); + if (sb.length() > 0) { + sb.append(" + "); + } + sb.append(val); + } + } + return sb.length() > 0 ? sb.toString() : "\"\""; + } + + private String visitMapExpression(final MapExpression expr, final String bindingVar) { + final List<MapEntryExpression> entries = expr.getMapEntryExpressions(); + if (entries.isEmpty()) { + return "java.util.Collections.emptyMap()"; + } + if (entries.size() == 1) { + final MapEntryExpression e = entries.get(0); + return "Map.of(" + visitValueExpression(e.getKeyExpression(), bindingVar) + + ", " + visitValueExpression(e.getValueExpression(), bindingVar) + ")"; + } + final StringBuilder sb = new StringBuilder("Map.of("); + for (int i = 0; i < entries.size(); i++) { + if (i > 0) { + sb.append(", "); + } + final MapEntryExpression e = entries.get(i); + sb.append(visitValueExpression(e.getKeyExpression(), bindingVar)); + sb.append(", "); + sb.append(visitValueExpression(e.getValueExpression(), bindingVar)); + } + sb.append(")"); + return sb.toString(); + } + + // ---- Helpers ---- + + private List<Statement> flattenStatements(final Statement stmt) { + if (stmt instanceof BlockStatement) { + return ((BlockStatement) stmt).getStatements(); + } + return List.of(stmt); + } + + private boolean isThisOrImplicit(final Expression expr) { + if (expr instanceof VariableExpression) { + final String name = ((VariableExpression) expr).getName(); + return "this".equals(name); + } + return false; + } + + private String lambdaVarFor(final String methodName) { + switch (methodName) { + case "extractor": return "ext"; + case "sink": return "s"; + case "slowSql": return "sql"; + case "sampledTrace": return "st"; + case "metrics": return "m"; + case "sampler": return "sp"; + case "text": return "tp"; + default: return "x"; + } + } + + private String childReceiverFor(final String lambdaVar) { + return lambdaVar; + } + + private ArgumentListExpression toArgList(final Expression args) { + if (args instanceof ArgumentListExpression) { + return (ArgumentListExpression) args; + } + if (args instanceof TupleExpression) { + final ArgumentListExpression ale = new ArgumentListExpression(); + for (Expression e : ((TupleExpression) args).getExpressions()) { + ale.addExpression(e); + } + return ale; + } + final ArgumentListExpression ale = new ArgumentListExpression(); + ale.addExpression(args); + return ale; + } + + private static String capitalize(final String s) { + if (s == null || s.isEmpty()) { + return s; + } + return Character.toUpperCase(s.charAt(0)) + s.substring(1); + } + + private static void indent(final StringBuilder sb, final int level) { + for (int i = 0; i < level; i++) { + sb.append(" "); + } + } + + // ---- Compilation & Manifest ---- + + /** + * Compile all registered LAL sources using javax.tools.JavaCompiler. + */ + public void compileAll(final File sourceDir, final File outputDir, + final String classpath) throws IOException { + if (lalSources.isEmpty()) { + log.info("No LAL sources to compile."); + return; + } + + final String packageDir = GENERATED_PACKAGE.replace('.', File.separatorChar); + final 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); + } + + final List<File> javaFiles = new ArrayList<>(); + for (Map.Entry<String, String> entry : lalSources.entrySet()) { + final File javaFile = new File(srcPkgDir, entry.getKey() + ".java"); + Files.writeString(javaFile.toPath(), entry.getValue()); + javaFiles.add(javaFile); + } + + final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + if (compiler == null) { + throw new IllegalStateException("No Java compiler available — requires JDK"); + } + + final StringWriter errorWriter = new StringWriter(); + + try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) { + final Iterable<? extends JavaFileObject> compilationUnits = + fileManager.getJavaFileObjectsFromFiles(javaFiles); + + final List<String> options = Arrays.asList( + "-d", outputDir.getAbsolutePath(), + "-classpath", classpath + ); + + final JavaCompiler.CompilationTask task = compiler.getTask( + errorWriter, fileManager, null, options, null, compilationUnits); + + final boolean success = task.call(); + if (!success) { + for (Map.Entry<String, String> entry : lalSources.entrySet()) { + log.error("Generated source for {}:\n{}", entry.getKey(), entry.getValue()); + } + throw new RuntimeException( + "Java compilation failed for " + javaFiles.size() + " LAL sources:\n" + + errorWriter); + } + } + + log.info("Compiled {} LAL sources to {}", lalSources.size(), outputDir); + } + + /** + * Write lal-expressions.txt manifest: hash=FQCN format. + */ + public void writeManifest(final File outputDir) throws IOException { + final File manifestDir = new File(outputDir, "META-INF"); + if (!manifestDir.exists() && !manifestDir.mkdirs()) { + throw new IOException("Failed to create META-INF dir: " + manifestDir); + } + + final List<String> lines = hashToClass.entrySet().stream() + .map(e -> e.getKey() + "=" + e.getValue()) + .sorted() + .collect(Collectors.toList()); + Files.write(new File(manifestDir, "lal-expressions.txt").toPath(), lines); + log.info("Wrote lal-expressions.txt with {} entries", lines.size()); + } +} diff --git a/oap-server/analyzer/lal-transpiler/src/test/java/org/apache/skywalking/oap/server/transpiler/lal/LalToJavaTranspilerTest.java b/oap-server/analyzer/lal-transpiler/src/test/java/org/apache/skywalking/oap/server/transpiler/lal/LalToJavaTranspilerTest.java new file mode 100644 index 0000000000..4a580a4bcd --- /dev/null +++ b/oap-server/analyzer/lal-transpiler/src/test/java/org/apache/skywalking/oap/server/transpiler/lal/LalToJavaTranspilerTest.java @@ -0,0 +1,678 @@ +/* + * 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.transpiler.lal; + +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; + +class LalToJavaTranspilerTest { + + private LalToJavaTranspiler transpiler; + + @BeforeEach + void setUp() { + transpiler = new LalToJavaTranspiler(); + } + + // ---- Class Structure ---- + + @Test + void classStructure() { + final String java = transpiler.transpile("LalExpr_test", + "filter { json {} }"); + assertNotNull(java); + + assertTrue(java.contains("package " + LalToJavaTranspiler.GENERATED_PACKAGE), + "Should have correct package"); + assertTrue(java.contains("public class LalExpr_test implements LalExpression"), + "Should implement LalExpression"); + assertTrue(java.contains("public void execute(FilterSpec filterSpec, Binding binding)"), + "Should have execute method"); + } + + @Test + void helperMethodsPresent() { + final String java = transpiler.transpile("LalExpr_test", + "filter { json {} }"); + assertNotNull(java); + + assertTrue(java.contains("private static Object getAt(Object obj, String key)"), + "Should have getAt helper"); + assertTrue(java.contains("private static long toLong(Object obj)"), + "Should have toLong helper"); + assertTrue(java.contains("private static int toInt(Object obj)"), + "Should have toInt helper"); + assertTrue(java.contains("private static boolean toBoolean(Object obj)"), + "Should have toBoolean helper"); + assertTrue(java.contains("private static boolean isTruthy(Object obj)"), + "Should have isTruthy helper"); + assertTrue(java.contains("private static boolean isNonEmptyString(Object obj)"), + "Should have isNonEmptyString helper"); + } + + @Test + void importsPresent() { + final String java = transpiler.transpile("LalExpr_test", + "filter { json {} }"); + assertNotNull(java); + + assertTrue(java.contains("import org.apache.skywalking.oap.log.analyzer.dsl.Binding;"), + "Should import Binding"); + assertTrue(java.contains("import org.apache.skywalking.oap.log.analyzer.dsl.LalExpression;"), + "Should import LalExpression"); + assertTrue(java.contains("import org.apache.skywalking.oap.log.analyzer.dsl.spec.filter.FilterSpec;"), + "Should import FilterSpec"); + } + + // ---- filter {} Unwrapping ---- + + @Test + void filterUnwrap() { + final String java = transpiler.transpile("LalExpr_test", + "filter { json {} }"); + assertNotNull(java); + + assertTrue(java.contains("filterSpec.json()"), + "Should unwrap filter block and call json() on filterSpec"); + assertTrue(!java.contains("filterSpec.filter("), + "Should NOT emit filterSpec.filter() call"); + } + + // ---- json {} ---- + + @Test + void jsonEmptyBlock() { + final String java = transpiler.transpile("LalExpr_test", + "filter { json {} }"); + assertNotNull(java); + + assertTrue(java.contains("filterSpec.json();"), + "Should emit no-arg json() call"); + } + + // ---- text { regexp $/pattern/$ } ---- + + @Test + void textWithRegexp() { + final String java = transpiler.transpile("LalExpr_test", + "filter { text { regexp $/(?<timestamp>\\d+)\\s+(?<service>.+)/$ } }"); + assertNotNull(java); + + assertTrue(java.contains("filterSpec.text(tp -> {"), + "Should emit text with Consumer lambda"); + assertTrue(java.contains("tp.regexp("), + "Should call regexp on text parser spec"); + } + + // ---- extractor { ... } ---- + + @Test + void extractorWithService() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " extractor {\n" + + " service parsed.service as String\n" + + " }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("filterSpec.extractor(ext -> {"), + "Should emit extractor with Consumer lambda"); + assertTrue(java.contains("ext.service("), + "Should call service on extractor spec"); + assertTrue(java.contains("String.valueOf("), + "Should use String.valueOf for 'as String' cast"); + assertTrue(java.contains("getAt(binding.parsed(), \"service\")"), + "Should use getAt for parsed.service"); + } + + @Test + void extractorWithMultipleFields() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " extractor {\n" + + " service parsed.service as String\n" + + " instance parsed.instance as String\n" + + " endpoint parsed.endpoint as String\n" + + " layer parsed.layer as String\n" + + " timestamp parsed.time as String\n" + + " traceId parsed.traceId as String\n" + + " }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("ext.service(String.valueOf(getAt(binding.parsed(), \"service\")))"), + "Should extract service"); + assertTrue(java.contains("ext.instance(String.valueOf(getAt(binding.parsed(), \"instance\")))"), + "Should extract instance"); + assertTrue(java.contains("ext.endpoint(String.valueOf(getAt(binding.parsed(), \"endpoint\")))"), + "Should extract endpoint"); + assertTrue(java.contains("ext.layer(String.valueOf(getAt(binding.parsed(), \"layer\")))"), + "Should extract layer"); + assertTrue(java.contains("ext.timestamp(String.valueOf(getAt(binding.parsed(), \"time\")))"), + "Should extract timestamp"); + assertTrue(java.contains("ext.traceId(String.valueOf(getAt(binding.parsed(), \"traceId\")))"), + "Should extract traceId"); + } + + // ---- sink { ... } ---- + + @Test + void sinkWithEnforcer() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " sink {\n" + + " enforcer {}\n" + + " }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("filterSpec.sink(s -> {"), + "Should emit sink with Consumer lambda"); + assertTrue(java.contains("s.enforcer();"), + "Should emit no-arg enforcer() on sink spec"); + } + + @Test + void sinkWithDropper() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " sink {\n" + + " dropper {}\n" + + " }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("s.dropper();"), + "Should emit no-arg dropper() on sink spec"); + } + + @Test + void sinkWithSampler() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " sink {\n" + + " sampler {\n" + + " rateLimit('abc')\n" + + " }\n" + + " }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("s.sampler(sp -> {"), + "Should emit sampler with Consumer lambda"); + assertTrue(java.contains("sp.rateLimit(\"abc\")"), + "Should call rateLimit on sampler spec"); + } + + // ---- abort {} ---- + + @Test + void abortBlock() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " abort {}\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("filterSpec.abort();"), + "Should emit no-arg abort() call"); + } + + // ---- parsed access ---- + + @Test + void parsedPropertyAccess() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " extractor { service parsed.service as String }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("binding.parsed()"), + "Should translate 'parsed' to binding.parsed()"); + assertTrue(java.contains("getAt(binding.parsed(), \"service\")"), + "Should use getAt for property access"); + } + + @Test + void parsedNestedAccess() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " extractor { service parsed.data.serviceName as String }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("getAt(getAt(binding.parsed(), \"data\"), \"serviceName\")"), + "Should translate nested parsed access to nested getAt()"); + } + + // ---- Safe navigation (?.) ---- + + @Test + void safeNavigationOnParsed() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " extractor { service parsed?.service as String }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("== null ? null : getAt("), + "Should translate ?. to null-safe ternary with getAt"); + } + + // ---- as Cast ---- + + @Test + void castAsString() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " extractor { service parsed.service as String }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("String.valueOf("), + "Should translate 'as String' to String.valueOf()"); + } + + @Test + void castAsLong() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " extractor { timestamp parsed.time as Long }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("toLong("), + "Should translate 'as Long' to toLong()"); + } + + // ---- log access ---- + + @Test + void logPropertyAccess() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " extractor { service log.service }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("binding.log().getService()"), + "Should translate log.service to binding.log().getService()"); + } + + // ---- if/else ---- + + @Test + void ifStatement() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " if (parsed.level == 'ERROR') {\n" + + " abort {}\n" + + " }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("if (\"ERROR\".equals(getAt(binding.parsed(), \"level\"))"), + "Should translate == with constant on left for null-safety"); + assertTrue(java.contains("filterSpec.abort()"), + "Should emit abort in if body"); + } + + @Test + void ifElseStatement() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " if (parsed.type == 'access') {\n" + + " extractor { layer 'HTTP' }\n" + + " } else {\n" + + " extractor { layer 'GENERAL' }\n" + + " }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("if (\"access\".equals(getAt(binding.parsed(), \"type\"))"), + "Should have if condition"); + assertTrue(java.contains("} else {"), + "Should have else block"); + } + + @Test + void ifElseIfStatement() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " if (parsed.type == 'a') {\n" + + " extractor { layer 'A' }\n" + + " } else if (parsed.type == 'b') {\n" + + " extractor { layer 'B' }\n" + + " } else {\n" + + " extractor { layer 'C' }\n" + + " }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("} else if ("), + "Should produce else-if chain"); + } + + // ---- Condition operators ---- + + @Test + void notEqualCondition() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " if (parsed.status != 'ok') { abort {} }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("!\"ok\".equals(getAt(binding.parsed(), \"status\"))"), + "Should translate != with negated .equals()"); + } + + @Test + void logicalAndCondition() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " if (parsed.a == 'x' && parsed.b == 'y') { abort {} }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("\"x\".equals(getAt(binding.parsed(), \"a\")) && \"y\".equals(getAt(binding.parsed(), \"b\"))"), + "Should translate && with .equals() on both sides"); + } + + @Test + void logicalOrCondition() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " if (parsed.a == 'x' || parsed.a == 'y') { abort {} }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("\"x\".equals(") && java.contains("|| \"y\".equals("), + "Should translate || correctly"); + } + + @Test + void truthinessCondition() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " if (parsed.value) { abort {} }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("isTruthy(getAt(binding.parsed(), \"value\"))"), + "Should translate bare expression to isTruthy()"); + } + + @Test + void negationCondition() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " if (!parsed.value) { abort {} }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("!isTruthy(getAt(binding.parsed(), \"value\"))"), + "Should translate !expr to negated isTruthy()"); + } + + @Test + void lessThanCondition() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " if (parsed.code < 400) { abort {} }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("toInt(getAt(binding.parsed(), \"code\")) < 400"), + "Should translate < with toInt on left"); + } + + @Test + void greaterThanEqualCondition() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " if (parsed.code >= 500) { abort {} }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("toInt(getAt(binding.parsed(), \"code\")) >= 500"), + "Should translate >= with toInt on left"); + } + + // ---- GString interpolation ---- + + @Test + void gstringInterpolation() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " extractor { service \"svc::${parsed.name}\" as String }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("\"svc::\""), + "Should have string prefix"); + assertTrue(java.contains("getAt(binding.parsed(), \"name\")"), + "Should have parsed access in interpolation"); + } + + // ---- tag(Map) ---- + + @Test + void tagWithMapLiteral() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " extractor {\n" + + " tag(status: parsed.status as String)\n" + + " }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("ext.tag("), + "Should call tag on extractor spec"); + } + + // ---- slowSql / sampledTrace / metrics ---- + + @Test + void slowSqlBlock() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " extractor {\n" + + " layer 'MYSQL'\n" + + " service parsed.service as String\n" + + " slowSql {\n" + + " id parsed.id as String\n" + + " statement parsed.statement as String\n" + + " latency parsed.latency as Long\n" + + " }\n" + + " }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("ext.slowSql(sql -> {"), + "Should emit slowSql with Consumer lambda, var 'sql'"); + assertTrue(java.contains("sql.id("), + "Should call id on slowSql spec"); + assertTrue(java.contains("sql.statement("), + "Should call statement on slowSql spec"); + assertTrue(java.contains("sql.latency(toLong("), + "Should call latency with toLong"); + } + + @Test + void sampledTraceBlock() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " extractor {\n" + + " sampledTrace {\n" + + " uri parsed.uri as String\n" + + " latency parsed.latency as Long\n" + + " }\n" + + " }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("ext.sampledTrace(st -> {"), + "Should emit sampledTrace with Consumer lambda, var 'st'"); + assertTrue(java.contains("st.uri("), + "Should call uri on sampledTrace spec"); + } + + @Test + void metricsBlock() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " extractor {\n" + + " metrics {\n" + + " name 'log_count'\n" + + " value 1\n" + + " }\n" + + " }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("ext.metrics(m -> {"), + "Should emit metrics with Consumer lambda, var 'm'"); + assertTrue(java.contains("m.name(\"log_count\")"), + "Should call name on metrics spec"); + assertTrue(java.contains("m.value(1)"), + "Should call value on metrics spec"); + } + + // ---- Complete LAL Script ---- + + @Test + void completeScript() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " extractor {\n" + + " service parsed.service as String\n" + + " instance parsed.instance as String\n" + + " layer parsed.layer as String\n" + + " timestamp parsed.time as String\n" + + " }\n" + + " sink {\n" + + " enforcer {}\n" + + " }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("filterSpec.json();"), + "Should have json() call"); + assertTrue(java.contains("filterSpec.extractor(ext -> {"), + "Should have extractor block"); + assertTrue(java.contains("filterSpec.sink(s -> {"), + "Should have sink block"); + assertTrue(java.contains("s.enforcer();"), + "Should have enforcer in sink"); + } + + // ---- tag("KEY") as value ---- + + @Test + void tagAsValue() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " if (tag('status') == 'error') { abort {} }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("filterSpec.tag(\"status\")"), + "Should call tag on filterSpec"); + } + + // ---- rateLimit ---- + + @Test + void rateLimitWithClosureArg() { + final String java = transpiler.transpile("LalExpr_test", + "filter {\n" + + " json {}\n" + + " sink {\n" + + " sampler {\n" + + " rateLimit('myId') { rpm 5 }\n" + + " }\n" + + " }\n" + + "}"); + assertNotNull(java); + + assertTrue(java.contains("sp.rateLimit(\"myId\", rls -> {"), + "Should emit rateLimit with id and closure lambda"); + assertTrue(java.contains("rls.rpm(5)"), + "Should call rpm on rate limit spec"); + } + + // ---- Manifest ---- + + @Test + void manifest(@TempDir Path tempDir) throws Exception { + final String source = transpiler.transpile("LalExpr_a", "filter { json {} }"); + transpiler.register("LalExpr_a", "abc123hash", source); + + final File outputDir = tempDir.toFile(); + transpiler.writeManifest(outputDir); + + final File manifest = new File(outputDir, "META-INF/lal-expressions.txt"); + assertTrue(manifest.exists(), "Manifest file should exist"); + + final List<String> lines = Files.readAllLines(manifest.toPath()); + assertTrue(lines.stream().anyMatch(l -> l.contains("abc123hash") && + l.contains(LalToJavaTranspiler.GENERATED_PACKAGE + ".LalExpr_a")), + "Should contain hash=FQCN mapping"); + } +} diff --git a/oap-server/analyzer/mal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/mal/MalToJavaTranspiler.java b/oap-server/analyzer/mal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/mal/MalToJavaTranspiler.java index c256bb589c..d7632688e1 100644 --- a/oap-server/analyzer/mal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/mal/MalToJavaTranspiler.java +++ b/oap-server/analyzer/mal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/mal/MalToJavaTranspiler.java @@ -1089,7 +1089,7 @@ public class MalToJavaTranspiler { // ---- Utility ---- - static String escapeJava(final String s) { + public static String escapeJava(final String s) { return s.replace("\\", "\\\\") .replace("\"", "\\\"") .replace("\n", "\\n") diff --git a/oap-server/analyzer/pom.xml b/oap-server/analyzer/pom.xml index 4039928a50..1c2a04f34d 100644 --- a/oap-server/analyzer/pom.xml +++ b/oap-server/analyzer/pom.xml @@ -34,6 +34,7 @@ <module>meter-analyzer</module> <module>event-analyzer</module> <module>mal-transpiler</module> + <module>lal-transpiler</module> </modules> <dependencies>
