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/03/03 12:56:00 UTC
(skywalking) 15/27: Fix all MAL/LAL v2 compiler gaps and add runtime execution comparison
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 2e7ae6326aaecd1a22e80f46858e5a90485b4056
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Sun Mar 1 22:38:15 2026 +0800
Fix all MAL/LAL v2 compiler gaps and add runtime execution comparison
MAL compiler fixes (closes 38 previously failing expressions):
- Add ternary operator (?:) support in closures (grammar, AST, codegen)
- Fix valueEqual() and other primitive-double methods with numeric literal args
- Support double-paren argument syntax: sum((['cluster']))
- Handle NUMBER / SampleFamily via MalRuntimeHelper.divReverse() in v2 package
- Add variable declarations, map literals, forEach/instance closure types
- Add ProcessRegistry class references, improved safe navigation
LAL compiler fixes:
- Fix null-to-string conversion: use null-safe toStr() instead of String.valueOf()
- Add camelToSnake field name fallback for protobuf field access
- Add typed execute(FilterSpec, Binding) method signature
- Reorganize LAL test scripts into oap-cases/ and feature-cases/
- Add data-driven LALExpressionExecutionTest with 27 test cases
MAL checker enhancements:
- Add runtime execution comparison (mock SampleFamily data, execute both
v1 and v2, compare output samples with labels and values)
- Handle increase()/rate() by priming CounterWindow with initial run
- Extract tagEqual patterns from expressions for matching mock data
All 1,187 MAL + 29 LAL + 22 hierarchy expressions now pass with zero gaps.
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
oap-server/analyzer/log-analyzer/pom.xml | 6 +
.../log/analyzer/compiler/LALClassGenerator.java | 24 +-
.../oap/log/analyzer/compiler/LALScriptModel.java | 2 +-
.../oap/log/analyzer/compiler/LALScriptParser.java | 8 +
.../skywalking/oap/log/analyzer/dsl/Binding.java | 30 +-
.../compiler/LALExpressionExecutionTest.java | 478 +++++++++++++++++++++
.../apache/skywalking/mal/rt/grammar/MALParser.g4 | 32 +-
.../meter/analyzer/compiler/MALClassGenerator.java | 306 +++++++++++--
.../analyzer/compiler/MALExpressionModel.java | 102 ++++-
.../meter/analyzer/compiler/MALScriptParser.java | 80 +++-
.../analyzer/compiler/rt/MalRuntimeHelper.java | 52 +++
.../analyzer/compiler/MALClassGeneratorTest.java | 192 +++++++++
.../analyzer/compiler/MALScriptParserTest.java | 121 ++++++
oap-server/server-starter/pom.xml | 7 +
.../oap/server/checker/lal/LalComparisonTest.java | 44 +-
.../oap/server/checker/mal/MalComparisonTest.java | 236 +++++++++-
.../feature-cases/execution-basic.input.data | 157 +++++++
.../test-lal/feature-cases/execution-basic.yaml | 272 ++++++++++++
.../{default.yaml => oap-cases/default.input.data} | 17 +-
.../lal/test-lal/{ => oap-cases}/default.yaml | 0
.../envoy-als.input.data} | 21 +-
.../lal/test-lal/{ => oap-cases}/envoy-als.yaml | 0
.../lal/test-lal/oap-cases/k8s-service.input.data | 39 ++
.../lal/test-lal/{ => oap-cases}/k8s-service.yaml | 0
.../lal/test-lal/oap-cases/mesh-dp.input.data | 39 ++
.../lal/test-lal/{ => oap-cases}/mesh-dp.yaml | 0
.../mysql-slowsql.input.data} | 20 +-
.../test-lal/{ => oap-cases}/mysql-slowsql.yaml | 0
.../{default.yaml => oap-cases/nginx.input.data} | 18 +-
.../lal/test-lal/{ => oap-cases}/nginx.yaml | 0
.../pgsql-slowsql.input.data} | 20 +-
.../test-lal/{ => oap-cases}/pgsql-slowsql.yaml | 0
.../redis-slowsql.input.data} | 20 +-
.../test-lal/{ => oap-cases}/redis-slowsql.yaml | 0
34 files changed, 2199 insertions(+), 144 deletions(-)
diff --git a/oap-server/analyzer/log-analyzer/pom.xml b/oap-server/analyzer/log-analyzer/pom.xml
index ff811b1596..180a8435df 100644
--- a/oap-server/analyzer/log-analyzer/pom.xml
+++ b/oap-server/analyzer/log-analyzer/pom.xml
@@ -55,6 +55,12 @@
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.skywalking</groupId>
+ <artifactId>receiver-proto</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALClassGenerator.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALClassGenerator.java
index be5e78d9ef..94895706f6 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALClassGenerator.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALClassGenerator.java
@@ -507,8 +507,10 @@ public final class LALClassGenerator {
final ConsumerInfo parentInfo,
final int[] subCounter) {
for (final LALScriptModel.FilterStatement stmt : stmts) {
- if (stmt instanceof LALScriptModel.FieldAssignment) {
- // SampledTrace fields (processId, latency, etc.) are parsed as FieldAssignment
+ if (stmt instanceof LALScriptModel.SampledTraceField) {
+ generateSampledTraceField(sb,
+ (LALScriptModel.SampledTraceField) stmt);
+ } else if (stmt instanceof LALScriptModel.FieldAssignment) {
generateSampledTraceFieldFromAssignment(sb,
(LALScriptModel.FieldAssignment) stmt);
} else if (stmt instanceof LALScriptModel.IfBlock) {
@@ -767,11 +769,8 @@ public final class LALClassGenerator {
private String generateExecuteMethod(final LALScriptModel model,
final int[] counter) {
final StringBuilder sb = new StringBuilder();
- sb.append("public void execute(Object arg0, Object arg1) {\n");
- sb.append(" ").append(FILTER_SPEC).append(" filterSpec = (")
- .append(FILTER_SPEC).append(") arg0;\n");
- sb.append(" ").append(BINDING).append(" binding = (")
- .append(BINDING).append(") arg1;\n");
+ sb.append("public void execute(").append(FILTER_SPEC)
+ .append(" filterSpec, ").append(BINDING).append(" binding) {\n");
for (final LALScriptModel.FilterStatement stmt
: model.getStatements()) {
@@ -861,6 +860,8 @@ public final class LALClassGenerator {
+ " return ((" + BINDING_PARSED + ") obj).getAt(key);"
+ " if (obj instanceof java.util.Map)"
+ " return ((java.util.Map) obj).get(key);"
+ + " Object protoResult = " + BINDING_PARSED + ".getField(obj, key);"
+ + " if (protoResult != null) return protoResult;"
+ " return null;"
+ "}", ctClass));
@@ -878,6 +879,11 @@ public final class LALClassGenerator {
+ " return 0;"
+ "}", ctClass));
+ ctClass.addMethod(CtNewMethod.make(
+ "private static String toStr(Object obj) {"
+ + " return obj == null ? null : String.valueOf(obj);"
+ + "}", ctClass));
+
ctClass.addMethod(CtNewMethod.make(
"private static boolean toBool(Object obj) {"
+ " if (obj instanceof Boolean) return ((Boolean) obj).booleanValue();"
@@ -1044,7 +1050,7 @@ public final class LALClassGenerator {
final LALScriptModel.ValueAccess value,
final String castType) {
if ("String".equals(castType)) {
- sb.append("String.valueOf(");
+ sb.append("toStr(");
generateValueAccess(sb, value);
sb.append(")");
} else if ("Long".equals(castType)) {
@@ -1068,7 +1074,7 @@ public final class LALClassGenerator {
final LALScriptModel.ValueAccess value,
final String castType) {
if ("String".equals(castType)) {
- sb.append("String.valueOf(");
+ sb.append("toStr(");
generateValueAccess(sb, value);
sb.append(")");
} else {
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptModel.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptModel.java
index 8609deb66d..f20a66abdd 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptModel.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptModel.java
@@ -197,7 +197,7 @@ public final class LALScriptModel {
}
@Getter
- public static final class SampledTraceField implements SampledTraceStatement {
+ public static final class SampledTraceField implements SampledTraceStatement, FilterStatement {
private final SampledTraceFieldType fieldType;
private final ValueAccess value;
private final String castType;
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptParser.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptParser.java
index 2793ed4dde..e9128221b3 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptParser.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptParser.java
@@ -499,6 +499,14 @@ public final class LALScriptParser {
stmts.add((FilterStatement) new DropperStatement());
}
}
+ for (final LALParser.SampledTraceStatementContext stc :
+ ctx.sampledTraceStatement()) {
+ if (stc.ifStatement() != null) {
+ stmts.add((FilterStatement) visitIfStatement(stc.ifStatement()));
+ } else {
+ stmts.add((FilterStatement) visitSampledTraceField(stc));
+ }
+ }
return stmts;
}
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/Binding.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/Binding.java
index 41404fbdb0..3c60d3df3a 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/Binding.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/Binding.java
@@ -206,22 +206,46 @@ public class Binding {
return null;
}
- static Object getField(final Object obj, final String name) {
+ public static Object getField(final Object obj, final String name) {
if (obj instanceof Message) {
- final Descriptors.FieldDescriptor fd =
+ Descriptors.FieldDescriptor fd =
((Message) obj).getDescriptorForType().findFieldByName(name);
+ if (fd == null) {
+ fd = ((Message) obj).getDescriptorForType()
+ .findFieldByName(camelToSnake(name));
+ }
if (fd != null) {
return ((Message) obj).getField(fd);
}
}
if (obj instanceof Message.Builder) {
- final Descriptors.FieldDescriptor fd =
+ Descriptors.FieldDescriptor fd =
((Message.Builder) obj).getDescriptorForType().findFieldByName(name);
+ if (fd == null) {
+ fd = ((Message.Builder) obj).getDescriptorForType()
+ .findFieldByName(camelToSnake(name));
+ }
if (fd != null) {
return ((Message.Builder) obj).getField(fd);
}
}
return null;
}
+
+ private static String camelToSnake(final String name) {
+ final StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < name.length(); i++) {
+ final char c = name.charAt(i);
+ if (Character.isUpperCase(c)) {
+ if (i > 0) {
+ sb.append('_');
+ }
+ sb.append(Character.toLowerCase(c));
+ } else {
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
}
}
diff --git a/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/compiler/LALExpressionExecutionTest.java b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/compiler/LALExpressionExecutionTest.java
new file mode 100644
index 0000000000..30ed016d2c
--- /dev/null
+++ b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/compiler/LALExpressionExecutionTest.java
@@ -0,0 +1,478 @@
+/*
+ * 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.log.analyzer.compiler;
+
+import java.io.File;
+import java.lang.reflect.Field;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.google.protobuf.Message;
+import com.google.protobuf.util.JsonFormat;
+import org.apache.skywalking.apm.network.common.v3.KeyStringValuePair;
+import org.apache.skywalking.apm.network.logging.v3.JSONLog;
+import org.apache.skywalking.apm.network.logging.v3.LogData;
+import org.apache.skywalking.apm.network.logging.v3.LogDataBody;
+import org.apache.skywalking.apm.network.logging.v3.LogTags;
+import org.apache.skywalking.apm.network.logging.v3.TextLog;
+import org.apache.skywalking.oap.log.analyzer.dsl.Binding;
+import org.apache.skywalking.oap.log.analyzer.dsl.LalExpression;
+import org.apache.skywalking.oap.log.analyzer.dsl.spec.filter.FilterSpec;
+import org.apache.skywalking.oap.server.analyzer.provider.trace.parser.listener.SampledTraceBuilder;
+import org.apache.skywalking.oap.log.analyzer.module.LogAnalyzerModule;
+import org.apache.skywalking.oap.log.analyzer.provider.LogAnalyzerModuleConfig;
+import org.apache.skywalking.oap.log.analyzer.provider.LogAnalyzerModuleProvider;
+import org.apache.skywalking.oap.server.core.CoreModule;
+import org.apache.skywalking.oap.server.core.config.NamingControl;
+import org.apache.skywalking.oap.server.core.config.group.EndpointNameGrouping;
+import org.apache.skywalking.oap.server.core.config.ConfigService;
+import org.apache.skywalking.oap.server.core.source.SourceReceiver;
+import org.apache.skywalking.oap.server.library.module.ModuleManager;
+import org.apache.skywalking.oap.server.library.module.ModuleProviderHolder;
+import org.apache.skywalking.oap.server.library.module.ModuleServiceHolder;
+import org.junit.jupiter.api.DynamicTest;
+import org.junit.jupiter.api.TestFactory;
+import org.yaml.snakeyaml.Yaml;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Data-driven runtime execution tests for compiled LAL expressions.
+ *
+ * <p>Loads LAL rules from {@code .yaml} files and mock input from
+ * corresponding {@code .input.data} files in the {@code test-lal/}
+ * directory tree. For each rule that has a matching input entry,
+ * compiles the DSL via {@link LALClassGenerator}, executes it against
+ * a real {@link FilterSpec} + {@link Binding}, and asserts on the
+ * expected state defined in the {@code expect} block.
+ */
+class LALExpressionExecutionTest {
+
+ @TestFactory
+ Collection<DynamicTest> lalExecutionTests() throws Exception {
+ final List<DynamicTest> tests = new ArrayList<>();
+ final FilterSpec filterSpec = buildFilterSpec();
+ final LALClassGenerator generator = new LALClassGenerator();
+ final Yaml yaml = new Yaml();
+
+ final Path testLalDir = findTestLalDir();
+ if (testLalDir == null) {
+ return tests;
+ }
+
+ // Scan subdirectories (oap-cases/, feature-cases/)
+ final File[] subdirs = testLalDir.toFile().listFiles(File::isDirectory);
+ if (subdirs == null) {
+ return tests;
+ }
+
+ for (final File subdir : subdirs) {
+ final File[] files = subdir.listFiles();
+ if (files == null) {
+ continue;
+ }
+ for (final File yamlFile : files) {
+ if (!yamlFile.getName().endsWith(".yaml")
+ && !yamlFile.getName().endsWith(".yml")) {
+ continue;
+ }
+
+ // Look for matching .input.data file
+ final String baseName = yamlFile.getName()
+ .replaceAll("\\.(yaml|yml)$", "");
+ final File inputDataFile = new File(subdir,
+ baseName + ".input.data");
+ if (!inputDataFile.exists()) {
+ continue;
+ }
+
+ // Parse the YAML rules
+ final String yamlContent =
+ Files.readString(yamlFile.toPath());
+ final Map<String, Object> config = yaml.load(yamlContent);
+ if (config == null || !config.containsKey("rules")) {
+ continue;
+ }
+ @SuppressWarnings("unchecked")
+ final List<Map<String, String>> rules =
+ (List<Map<String, String>>) config.get("rules");
+ if (rules == null) {
+ continue;
+ }
+
+ // Parse the input data
+ final String inputContent =
+ Files.readString(inputDataFile.toPath());
+ @SuppressWarnings("unchecked")
+ final Map<String, Map<String, Object>> inputData =
+ yaml.load(inputContent);
+ if (inputData == null) {
+ continue;
+ }
+
+ final String category = subdir.getName();
+ for (final Map<String, String> rule : rules) {
+ final String ruleName = rule.get("name");
+ final String dsl = rule.get("dsl");
+ final String ruleLayer = rule.get("layer");
+ if (ruleName == null || dsl == null) {
+ continue;
+ }
+ final Map<String, Object> input =
+ inputData.get(ruleName);
+ if (input == null) {
+ continue;
+ }
+
+ tests.add(DynamicTest.dynamicTest(
+ category + "/" + baseName + " | " + ruleName,
+ () -> executeAndAssert(
+ generator, filterSpec, ruleName,
+ dsl, ruleLayer, input)
+ ));
+ }
+ }
+ }
+ return tests;
+ }
+
+ private void executeAndAssert(
+ final LALClassGenerator generator,
+ final FilterSpec filterSpec,
+ final String ruleName,
+ final String dsl,
+ final String ruleLayer,
+ final Map<String, Object> input) throws Exception {
+ final LalExpression expr = generator.compile(dsl);
+ final LogData.Builder logData = buildLogData(input);
+ if (ruleLayer != null) {
+ logData.setLayer(ruleLayer);
+ }
+ final Binding binding = new Binding();
+ binding.log(logData);
+
+ // Set proto extraLog if specified
+ final Message extraLog = buildExtraLog(input);
+ if (extraLog != null) {
+ binding.extraLog(extraLog);
+ }
+
+ filterSpec.bind(binding);
+ expr.execute(filterSpec, binding);
+
+ // Assert expected values
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> expect =
+ (Map<String, Object>) input.get("expect");
+ if (expect == null) {
+ return;
+ }
+
+ for (final Map.Entry<String, Object> entry : expect.entrySet()) {
+ final String key = entry.getKey();
+ final String expected = String.valueOf(entry.getValue());
+
+ switch (key) {
+ case "service":
+ assertEquals(expected, binding.log().getService(),
+ ruleName + ": service mismatch");
+ break;
+ case "instance":
+ assertEquals(expected,
+ binding.log().getServiceInstance(),
+ ruleName + ": serviceInstance mismatch");
+ break;
+ case "endpoint":
+ assertEquals(expected, binding.log().getEndpoint(),
+ ruleName + ": endpoint mismatch");
+ break;
+ case "layer":
+ assertEquals(expected, binding.log().getLayer(),
+ ruleName + ": layer mismatch");
+ break;
+ case "save":
+ assertEquals(Boolean.parseBoolean(expected),
+ binding.shouldSave(),
+ ruleName + ": shouldSave mismatch");
+ break;
+ case "abort":
+ assertEquals(Boolean.parseBoolean(expected),
+ binding.shouldAbort(),
+ ruleName + ": shouldAbort mismatch");
+ break;
+ case "timestamp":
+ assertEquals(Long.parseLong(expected),
+ binding.log().getTimestamp(),
+ ruleName + ": timestamp mismatch");
+ break;
+ default:
+ if (key.startsWith("sampledTrace.")) {
+ assertSampledTrace(
+ ruleName, key, expected, binding);
+ } else if (key.startsWith("tag.")) {
+ final String tagKey = key.substring(4);
+ final List<KeyStringValuePair> tags =
+ binding.log().getTags().getDataList();
+ assertTrue(tags.stream().anyMatch(
+ t -> tagKey.equals(t.getKey())
+ && expected.equals(t.getValue())),
+ ruleName + ": expected tag "
+ + tagKey + "=" + expected
+ + ", got: " + tags.stream()
+ .map(t -> t.getKey() + "=" + t.getValue())
+ .collect(Collectors.joining(", ")));
+ }
+ break;
+ }
+ }
+ }
+
+ // ==================== SampledTrace assertions ====================
+
+ private static void assertSampledTrace(
+ final String ruleName,
+ final String key,
+ final String expected,
+ final Binding binding) {
+ final SampledTraceBuilder builder =
+ binding.sampledTraceBuilder();
+ assertTrue(builder != null,
+ ruleName + ": sampledTraceBuilder is null"
+ + " but expected " + key + "=" + expected);
+
+ final String field = key.substring("sampledTrace.".length());
+ switch (field) {
+ case "latency":
+ assertEquals(Long.parseLong(expected),
+ builder.getLatency(),
+ ruleName + ": sampledTrace.latency mismatch");
+ break;
+ case "uri":
+ assertEquals(expected, builder.getUri(),
+ ruleName + ": sampledTrace.uri mismatch");
+ break;
+ case "reason":
+ assertEquals(expected,
+ builder.getReason().name(),
+ ruleName + ": sampledTrace.reason mismatch");
+ break;
+ case "processId":
+ assertEquals(expected, builder.getProcessId(),
+ ruleName + ": sampledTrace.processId mismatch");
+ break;
+ case "destProcessId":
+ assertEquals(expected, builder.getDestProcessId(),
+ ruleName + ": sampledTrace.destProcessId mismatch");
+ break;
+ case "detectPoint":
+ assertEquals(expected,
+ builder.getDetectPoint().name(),
+ ruleName + ": sampledTrace.detectPoint mismatch");
+ break;
+ case "componentId":
+ assertEquals(Integer.parseInt(expected),
+ builder.getComponentId(),
+ ruleName
+ + ": sampledTrace.componentId mismatch");
+ break;
+ default:
+ throw new IllegalArgumentException(
+ ruleName + ": unknown sampledTrace field: "
+ + field);
+ }
+ }
+
+ // ==================== LogData builder ====================
+
+ @SuppressWarnings("unchecked")
+ private static LogData.Builder buildLogData(
+ final Map<String, Object> input) {
+ final LogData.Builder builder = LogData.newBuilder();
+
+ final String service = (String) input.get("service");
+ if (service != null) {
+ builder.setService(service);
+ }
+
+ final String instance = (String) input.get("instance");
+ if (instance != null) {
+ builder.setServiceInstance(instance);
+ }
+
+ final String traceId = (String) input.get("trace-id");
+ if (traceId != null) {
+ builder.setTraceContext(
+ org.apache.skywalking.apm.network.logging.v3.TraceContext
+ .newBuilder().setTraceId(traceId));
+ }
+
+ final Object tsObj = input.get("timestamp");
+ if (tsObj != null) {
+ builder.setTimestamp(Long.parseLong(String.valueOf(tsObj)));
+ }
+
+ final String bodyType = (String) input.get("body-type");
+ final String body = (String) input.get("body");
+
+ if ("json".equals(bodyType) && body != null) {
+ builder.setBody(LogDataBody.newBuilder()
+ .setJson(JSONLog.newBuilder().setJson(body)));
+ } else if ("text".equals(bodyType) && body != null) {
+ builder.setBody(LogDataBody.newBuilder()
+ .setText(TextLog.newBuilder().setText(body)));
+ }
+
+ final Map<String, String> tags =
+ (Map<String, String>) input.get("tags");
+ if (tags != null && !tags.isEmpty()) {
+ final LogTags.Builder tagsBuilder = LogTags.newBuilder();
+ for (final Map.Entry<String, String> tag : tags.entrySet()) {
+ tagsBuilder.addData(KeyStringValuePair.newBuilder()
+ .setKey(tag.getKey())
+ .setValue(tag.getValue()));
+ }
+ builder.setTags(tagsBuilder);
+ }
+
+ return builder;
+ }
+
+ // ==================== Proto extraLog builder ====================
+
+ @SuppressWarnings("unchecked")
+ private static Message buildExtraLog(
+ final Map<String, Object> input) throws Exception {
+ final Map<String, String> extraLog =
+ (Map<String, String>) input.get("extra-log");
+ if (extraLog == null) {
+ return null;
+ }
+
+ final String protoClass = extraLog.get("proto-class");
+ final String protoJson = extraLog.get("proto-json");
+ if (protoClass == null || protoJson == null) {
+ return null;
+ }
+
+ final Class<?> clazz = Class.forName(protoClass);
+ final Message.Builder builder = (Message.Builder)
+ clazz.getMethod("newBuilder").invoke(null);
+ JsonFormat.parser()
+ .ignoringUnknownFields()
+ .merge(protoJson, builder);
+ return builder.build();
+ }
+
+ // ==================== FilterSpec setup ====================
+
+ private FilterSpec buildFilterSpec() throws Exception {
+ final ModuleManager manager = mock(ModuleManager.class);
+ setInternalField(manager, "isInPrepareStage", false);
+
+ when(manager.find(anyString()))
+ .thenReturn(mock(ModuleProviderHolder.class));
+
+ final ModuleProviderHolder logHolder =
+ mock(ModuleProviderHolder.class);
+ final LogAnalyzerModuleProvider logProvider =
+ mock(LogAnalyzerModuleProvider.class);
+ when(logProvider.getMetricConverts())
+ .thenReturn(Collections.emptyList());
+ when(logHolder.provider()).thenReturn(logProvider);
+ when(manager.find(LogAnalyzerModule.NAME)).thenReturn(logHolder);
+
+ final ModuleProviderHolder coreHolder =
+ mock(ModuleProviderHolder.class);
+ final ModuleServiceHolder coreServices =
+ mock(ModuleServiceHolder.class);
+ when(coreHolder.provider()).thenReturn(coreServices);
+ when(manager.find(CoreModule.NAME)).thenReturn(coreHolder);
+
+ when(coreServices.getService(SourceReceiver.class))
+ .thenReturn(mock(SourceReceiver.class));
+ when(coreServices.getService(NamingControl.class))
+ .thenReturn(new NamingControl(
+ 200, 200, 200, new EndpointNameGrouping()));
+ final ConfigService configService = mock(ConfigService.class);
+ when(configService.getSearchableLogsTags()).thenReturn("");
+ when(coreServices.getService(ConfigService.class))
+ .thenReturn(configService);
+
+ final FilterSpec filterSpec =
+ new FilterSpec(manager, new LogAnalyzerModuleConfig());
+ setInternalField(filterSpec, "sinkListenerFactories",
+ Collections.emptyList());
+
+ return filterSpec;
+ }
+
+ // ==================== Directory resolution ====================
+
+ private Path findTestLalDir() {
+ final String[] candidates = {
+ // From repo root (e.g., running with -pl from top level)
+ "test/script-cases/scripts/lal/test-lal",
+ // From oap-server/analyzer/log-analyzer/ module directory
+ "../../../test/script-cases/scripts/lal/test-lal",
+ // From script-runtime-with-groovy checker location
+ "../../scripts/lal/test-lal"
+ };
+ for (final String candidate : candidates) {
+ final Path path = Path.of(candidate);
+ if (Files.isDirectory(path)) {
+ return path;
+ }
+ }
+ return null;
+ }
+
+ // ==================== Reflection helpers ====================
+
+ private static void setInternalField(final Object target,
+ final String fieldName,
+ final Object value) {
+ try {
+ Field field = null;
+ Class<?> clazz = target.getClass();
+ while (clazz != null && field == null) {
+ try {
+ field = clazz.getDeclaredField(fieldName);
+ } catch (NoSuchFieldException e) {
+ clazz = clazz.getSuperclass();
+ }
+ }
+ if (field != null) {
+ field.setAccessible(true);
+ field.set(target, value);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "Failed to set field " + fieldName, e);
+ }
+ }
+}
diff --git a/oap-server/analyzer/meter-analyzer/src/main/antlr4/org/apache/skywalking/mal/rt/grammar/MALParser.g4 b/oap-server/analyzer/meter-analyzer/src/main/antlr4/org/apache/skywalking/mal/rt/grammar/MALParser.g4
index 0306ec4a2b..fac507dc01 100644
--- a/oap-server/analyzer/meter-analyzer/src/main/antlr4/org/apache/skywalking/mal/rt/grammar/MALParser.g4
+++ b/oap-server/analyzer/meter-analyzer/src/main/antlr4/org/apache/skywalking/mal/rt/grammar/MALParser.g4
@@ -89,6 +89,8 @@ argument
: additiveExpression // nested expression (metric ref, number, arithmetic)
| stringList // ["tag1", "tag2"]
| numberList // [50, 75, 90, 95, 99]
+ | L_PAREN stringList R_PAREN // (["tag1", "tag2"]) — extra parens
+ | L_PAREN numberList R_PAREN // ([50, 75, 90]) — extra parens
| closureExpression // {tags -> ...}
| enumRef // Layer.GENERAL, K8sRetagType.Pod2Service
| STRING // "PT1M", "k8s-key"
@@ -137,10 +139,17 @@ closureBody
closureStatement
: ifStatement
| returnStatement
+ | variableDeclaration
| assignmentStatement
| expressionStatement
;
+// ==================== Variable declarations ====================
+// Groovy-style: String result = "", String protocol = tags['protocol']
+variableDeclaration
+ : IDENTIFIER IDENTIFIER ASSIGN closureExpr SEMI?
+ ;
+
// ==================== Closure statements ====================
ifStatement
@@ -193,7 +202,9 @@ closureConditionPrimary
;
closureExpr
- : closureExpr PLUS closureExpr # closureAdd
+ : closureExpr QUESTION closureExpr COLON closureExpr # closureTernary
+ | closureExpr QUESTION COLON closureExpr # closureElvis
+ | closureExpr PLUS closureExpr # closureAdd
| closureExpr MINUS closureExpr # closureSub
| closureExpr STAR closureExpr # closureMul
| closureExpr SLASH closureExpr # closureDiv
@@ -205,11 +216,28 @@ closureExprPrimary
| NUMBER # closureNumber
| NULL # closureNull
| boolLiteral # closureBool
+ | closureMapLiteral # closureMap
| closureMethodChain # closureChain
+ | L_PAREN closureExpr R_PAREN # closureParen
+ ;
+
+// Groovy map literal: ['key': expr, 'key2': expr2]
+closureMapLiteral
+ : L_BRACKET closureMapEntry (COMMA closureMapEntry)* R_BRACKET
+ ;
+
+closureMapEntry
+ : STRING COLON closureExpr
;
closureMethodChain
- : closureTarget (DOT closureChainSegment)* (safeNav closureChainSegment)*
+ : closureTarget closureChainAccess*
+ ;
+
+closureChainAccess
+ : DOT closureChainSegment
+ | safeNav closureChainSegment
+ | L_BRACKET closureExpr R_BRACKET // direct bracket: tags['key']
;
closureTarget
diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/compiler/MALClassGenerator.java b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/compiler/MALClassGenerator.java
index e2f10c7966..81aea84996 100644
--- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/compiler/MALClassGenerator.java
+++ b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/compiler/MALClassGenerator.java
@@ -61,6 +61,11 @@ public final class MALClassGenerator {
*/
private static final java.util.Map<String, String> ENUM_FQCN;
+ /**
+ * Well-known helper classes used inside MAL closures (Groovy imports).
+ */
+ private static final java.util.Map<String, String> CLOSURE_CLASS_FQCN;
+
static {
ENUM_FQCN = new java.util.HashMap<>();
ENUM_FQCN.put("Layer", "org.apache.skywalking.oap.server.core.analysis.Layer");
@@ -69,13 +74,25 @@ public final class MALClassGenerator {
"org.apache.skywalking.oap.meter.analyzer.dsl.tagOpt.K8sRetagType");
ENUM_FQCN.put("DownsamplingType",
"org.apache.skywalking.oap.meter.analyzer.dsl.DownsamplingType");
+
+ CLOSURE_CLASS_FQCN = new java.util.HashMap<>();
+ CLOSURE_CLASS_FQCN.put("ProcessRegistry",
+ "org.apache.skywalking.oap.meter.analyzer.dsl.registry.ProcessRegistry");
}
private final ClassPool classPool;
private int closureCounter;
public MALClassGenerator() {
- this(ClassPool.getDefault());
+ this(createClassPool());
+ }
+
+ private static ClassPool createClassPool() {
+ final ClassPool pool = new ClassPool(true);
+ pool.appendClassPath(
+ new javassist.LoaderClassPath(
+ Thread.currentThread().getContextClassLoader()));
+ return pool;
}
public MALClassGenerator(final ClassPool classPool) {
@@ -253,28 +270,41 @@ public final class MALClassGenerator {
collectClosuresFromChain(
((MALExpressionModel.ParenChainExpr) expr).getMethodChain(), closures);
} else if (expr instanceof MALExpressionModel.FunctionCallExpr) {
- collectClosuresFromArgs(
- ((MALExpressionModel.FunctionCallExpr) expr).getArguments(), closures);
+ final MALExpressionModel.FunctionCallExpr fce =
+ (MALExpressionModel.FunctionCallExpr) expr;
+ collectClosuresFromArgs(fce.getFunctionName(),
+ fce.getArguments(), closures);
collectClosuresFromChain(
- ((MALExpressionModel.FunctionCallExpr) expr).getMethodChain(), closures);
+ fce.getMethodChain(), closures);
}
}
private void collectClosuresFromChain(final List<MALExpressionModel.MethodCall> chain,
final List<ClosureInfo> closures) {
for (final MALExpressionModel.MethodCall mc : chain) {
- collectClosuresFromArgs(mc.getArguments(), closures);
+ collectClosuresFromArgs(mc.getName(), mc.getArguments(), closures);
}
}
- private void collectClosuresFromArgs(final List<MALExpressionModel.Argument> args,
+ private void collectClosuresFromArgs(final String methodName,
+ final List<MALExpressionModel.Argument> args,
final List<ClosureInfo> closures) {
for (final MALExpressionModel.Argument arg : args) {
if (arg instanceof MALExpressionModel.ClosureArgument) {
+ final String interfaceType;
+ if ("forEach".equals(methodName)) {
+ interfaceType = "org.apache.skywalking.oap.meter.analyzer.dsl"
+ + ".SampleFamilyFunctions$ForEachFunction";
+ } else if ("instance".equals(methodName)) {
+ interfaceType = "org.apache.skywalking.oap.meter.analyzer.dsl"
+ + ".SampleFamilyFunctions$PropertiesExtractor";
+ } else {
+ interfaceType = "org.apache.skywalking.oap.meter.analyzer.dsl"
+ + ".SampleFamilyFunctions$TagFunction";
+ }
final ClosureInfo info = new ClosureInfo(
(MALExpressionModel.ClosureArgument) arg,
- "org.apache.skywalking.oap.meter.analyzer.dsl"
- + ".SampleFamilyFunctions$TagFunction");
+ interfaceType);
info.fieldIndex = closures.size();
closures.add(info);
} else if (arg instanceof MALExpressionModel.ExprArgument) {
@@ -284,6 +314,12 @@ public final class MALClassGenerator {
}
}
+ private static final String FOR_EACH_FUNCTION_TYPE =
+ "org.apache.skywalking.oap.meter.analyzer.dsl.SampleFamilyFunctions$ForEachFunction";
+
+ private static final String PROPERTIES_EXTRACTOR_TYPE =
+ "org.apache.skywalking.oap.meter.analyzer.dsl.SampleFamilyFunctions$PropertiesExtractor";
+
private Object compileClosureClass(final String className,
final ClosureInfo info) throws Exception {
final CtClass ctClass = classPool.makeClass(className);
@@ -292,22 +328,86 @@ public final class MALClassGenerator {
final MALExpressionModel.ClosureArgument closure = info.closure;
final List<String> params = closure.getParams();
- final String paramName = params.isEmpty() ? "it" : params.get(0);
+ final boolean isForEach = FOR_EACH_FUNCTION_TYPE.equals(info.interfaceType);
+ final boolean isPropertiesExtractor =
+ PROPERTIES_EXTRACTOR_TYPE.equals(info.interfaceType);
+
+ if (isForEach) {
+ // ForEachFunction: void accept(String element, Map<String, String> tags)
+ // Closure params: { prefix, tags -> ... } or { element, tags -> ... }
+ final String elementParam = params.size() >= 1 ? params.get(0) : "element";
+ final String tagsParam = params.size() >= 2 ? params.get(1) : "tags";
+
+ final StringBuilder sb = new StringBuilder();
+ sb.append("public void accept(String ").append(elementParam)
+ .append(", java.util.Map ").append(tagsParam).append(") {\n");
+ for (final MALExpressionModel.ClosureStatement stmt : closure.getBody()) {
+ generateClosureStatement(sb, stmt, tagsParam);
+ }
+ sb.append("}\n");
- final StringBuilder sb = new StringBuilder();
- sb.append("public java.util.Map apply(java.util.Map ").append(paramName)
- .append(") {\n");
- for (final MALExpressionModel.ClosureStatement stmt : closure.getBody()) {
- generateClosureStatement(sb, stmt, paramName);
- }
- sb.append(" return ").append(paramName).append(";\n");
- sb.append("}\n");
+ if (log.isDebugEnabled()) {
+ log.debug("ForEach closure body:\n{}", sb);
+ }
+ ctClass.addMethod(CtNewMethod.make(sb.toString(), ctClass));
+ } else if (isPropertiesExtractor) {
+ // PropertiesExtractor: Map<String,String> apply(Map<String,String> tags)
+ // Body is typically a single map literal expression
+ final String paramName = params.isEmpty() ? "it" : params.get(0);
+
+ final StringBuilder sb = new StringBuilder();
+ sb.append("public java.util.Map apply(java.util.Map ").append(paramName)
+ .append(") {\n");
+
+ // If the body is a single expression statement with a map literal,
+ // generate HashMap construction as the return value
+ final List<MALExpressionModel.ClosureStatement> body = closure.getBody();
+ if (body.size() == 1
+ && body.get(0) instanceof MALExpressionModel.ClosureExprStatement
+ && ((MALExpressionModel.ClosureExprStatement) body.get(0)).getExpr()
+ instanceof MALExpressionModel.ClosureMapLiteral) {
+ final MALExpressionModel.ClosureMapLiteral mapLit =
+ (MALExpressionModel.ClosureMapLiteral)
+ ((MALExpressionModel.ClosureExprStatement) body.get(0)).getExpr();
+ sb.append(" java.util.Map _result = new java.util.HashMap();\n");
+ for (final MALExpressionModel.MapEntry entry : mapLit.getEntries()) {
+ sb.append(" _result.put(\"")
+ .append(escapeJava(entry.getKey())).append("\", ");
+ generateClosureExpr(sb, entry.getValue(), paramName);
+ sb.append(");\n");
+ }
+ sb.append(" return _result;\n");
+ } else {
+ for (final MALExpressionModel.ClosureStatement stmt : body) {
+ generateClosureStatement(sb, stmt, paramName);
+ }
+ sb.append(" return ").append(paramName).append(";\n");
+ }
+ sb.append("}\n");
- // Also add the Object apply(Object) bridge method
- ctClass.addMethod(CtNewMethod.make(sb.toString(), ctClass));
- ctClass.addMethod(CtNewMethod.make(
- "public Object apply(Object o) { return apply((java.util.Map) o); }",
- ctClass));
+ ctClass.addMethod(CtNewMethod.make(sb.toString(), ctClass));
+ ctClass.addMethod(CtNewMethod.make(
+ "public Object apply(Object o) { return apply((java.util.Map) o); }",
+ ctClass));
+ } else {
+ // TagFunction: Map<String,String> apply(Map<String,String> tags)
+ final String paramName = params.isEmpty() ? "it" : params.get(0);
+
+ final StringBuilder sb = new StringBuilder();
+ sb.append("public java.util.Map apply(java.util.Map ").append(paramName)
+ .append(") {\n");
+ for (final MALExpressionModel.ClosureStatement stmt : closure.getBody()) {
+ generateClosureStatement(sb, stmt, paramName);
+ }
+ sb.append(" return ").append(paramName).append(";\n");
+ sb.append("}\n");
+
+ // Also add the Object apply(Object) bridge method
+ ctClass.addMethod(CtNewMethod.make(sb.toString(), ctClass));
+ ctClass.addMethod(CtNewMethod.make(
+ "public Object apply(Object o) { return apply((java.util.Map) o); }",
+ ctClass));
+ }
final Class<?> clazz = ctClass.toClass(MalExpressionPackageHolder.class);
ctClass.detach();
@@ -427,12 +527,10 @@ public final class MALClassGenerator {
sb.append(").multiply(Double.valueOf(").append(num).append("))");
break;
case DIV:
- sb.append("(");
+ sb.append("org.apache.skywalking.oap.meter.analyzer.compiler.rt")
+ .append(".MalRuntimeHelper.divReverse(").append(num).append(", ");
generateExpr(sb, right);
- sb.append(").newValue(new java.util.function.Function() { ")
- .append("public Object apply(Object v) { ")
- .append("return Double.valueOf(").append(num)
- .append(" / ((Double)v).doubleValue()); } })");
+ sb.append(")");
break;
default:
throw new IllegalArgumentException("Unsupported op: " + op);
@@ -463,6 +561,16 @@ public final class MALClassGenerator {
"tagEqual", "tagNotEqual", "tagMatch", "tagNotMatch"
);
+ /**
+ * Methods on SampleFamily whose first argument is a primitive {@code double}.
+ * Javassist cannot auto-unbox {@code Double} to {@code double}, so numeric
+ * arguments to these methods must be emitted as raw double literals.
+ */
+ private static final Set<String> PRIMITIVE_DOUBLE_METHODS = Set.of(
+ "valueEqual", "valueNotEqual", "valueGreater",
+ "valueGreaterEqual", "valueLess", "valueLessEqual"
+ );
+
private void generateMethodChain(final StringBuilder sb,
final List<MALExpressionModel.MethodCall> chain) {
for (final MALExpressionModel.MethodCall mc : chain) {
@@ -479,17 +587,41 @@ public final class MALClassGenerator {
}
sb.append('}');
} else {
+ final boolean primitiveDouble =
+ PRIMITIVE_DOUBLE_METHODS.contains(mc.getName());
for (int i = 0; i < args.size(); i++) {
if (i > 0) {
sb.append(", ");
}
- generateArgument(sb, args.get(i));
+ generateMethodCallArg(sb, args.get(i), primitiveDouble);
}
}
sb.append(')');
}
}
+ /**
+ * Generates a method call argument, handling numeric ExprArgument
+ * specially when the target method expects a primitive double.
+ */
+ private void generateMethodCallArg(final StringBuilder sb,
+ final MALExpressionModel.Argument arg,
+ final boolean primitiveDouble) {
+ if (primitiveDouble
+ && arg instanceof MALExpressionModel.ExprArgument) {
+ final MALExpressionModel.Expr innerExpr =
+ ((MALExpressionModel.ExprArgument) arg).getExpr();
+ if (innerExpr instanceof MALExpressionModel.NumberExpr) {
+ // Emit raw double literal for methods taking primitive double
+ final double num =
+ ((MALExpressionModel.NumberExpr) innerExpr).getValue();
+ sb.append(num);
+ return;
+ }
+ }
+ generateArgument(sb, arg);
+ }
+
private static boolean allStringArgs(final List<MALExpressionModel.Argument> args) {
for (final MALExpressionModel.Argument arg : args) {
if (!(arg instanceof MALExpressionModel.StringArgument)) {
@@ -547,7 +679,12 @@ public final class MALClassGenerator {
} else if (arg instanceof MALExpressionModel.ExprArgument) {
final MALExpressionModel.Expr innerExpr =
((MALExpressionModel.ExprArgument) arg).getExpr();
- if (innerExpr instanceof MALExpressionModel.MetricExpr
+ if (innerExpr instanceof MALExpressionModel.NumberExpr) {
+ // Numeric literal argument (e.g., valueEqual(1), multiply(100))
+ // Emit as Double.valueOf() to match Number parameter types.
+ final double num = ((MALExpressionModel.NumberExpr) innerExpr).getValue();
+ sb.append("Double.valueOf(").append(num).append(")");
+ } else if (innerExpr instanceof MALExpressionModel.MetricExpr
&& ((MALExpressionModel.MetricExpr) innerExpr).getMethodChain().isEmpty()) {
// Bare identifier — could be an enum constant like SUM, AVG
final String name =
@@ -578,8 +715,9 @@ public final class MALClassGenerator {
if (stmt instanceof MALExpressionModel.ClosureAssignment) {
final MALExpressionModel.ClosureAssignment assign =
(MALExpressionModel.ClosureAssignment) stmt;
- sb.append(" ").append(paramName).append(".put(\"")
- .append(escapeJava(assign.getTarget())).append("\", ");
+ sb.append(" ").append(assign.getMapVar()).append(".put(");
+ generateClosureExpr(sb, assign.getKeyExpr(), paramName);
+ sb.append(", ");
generateClosureExpr(sb, assign.getValue(), paramName);
sb.append(");\n");
} else if (stmt instanceof MALExpressionModel.ClosureIfStatement) {
@@ -600,9 +738,28 @@ public final class MALClassGenerator {
sb.append(" }\n");
}
} else if (stmt instanceof MALExpressionModel.ClosureReturnStatement) {
- sb.append(" return (java.util.Map) ");
- generateClosureExpr(sb,
- ((MALExpressionModel.ClosureReturnStatement) stmt).getValue(), paramName);
+ final MALExpressionModel.ClosureReturnStatement retStmt =
+ (MALExpressionModel.ClosureReturnStatement) stmt;
+ if (retStmt.getValue() == null) {
+ // Bare return (void return for ForEachFunction, or early exit)
+ sb.append(" return;\n");
+ } else {
+ sb.append(" return (java.util.Map) ");
+ generateClosureExpr(sb, retStmt.getValue(), paramName);
+ sb.append(";\n");
+ }
+ } else if (stmt instanceof MALExpressionModel.ClosureVarDecl) {
+ final MALExpressionModel.ClosureVarDecl vd =
+ (MALExpressionModel.ClosureVarDecl) stmt;
+ sb.append(" ").append(vd.getTypeName()).append(" ")
+ .append(vd.getVarName()).append(" = ");
+ generateClosureExpr(sb, vd.getInitializer(), paramName);
+ sb.append(";\n");
+ } else if (stmt instanceof MALExpressionModel.ClosureVarAssign) {
+ final MALExpressionModel.ClosureVarAssign va =
+ (MALExpressionModel.ClosureVarAssign) stmt;
+ sb.append(" ").append(va.getVarName()).append(" = ");
+ generateClosureExpr(sb, va.getValue(), paramName);
sb.append(";\n");
} else if (stmt instanceof MALExpressionModel.ClosureExprStatement) {
sb.append(" ");
@@ -625,6 +782,20 @@ public final class MALClassGenerator {
sb.append(((MALExpressionModel.ClosureBoolLiteral) expr).isValue());
} else if (expr instanceof MALExpressionModel.ClosureNullLiteral) {
sb.append("null");
+ } else if (expr instanceof MALExpressionModel.ClosureMapLiteral) {
+ // Inline map construction using java.util.Map.of()
+ final MALExpressionModel.ClosureMapLiteral mapLit =
+ (MALExpressionModel.ClosureMapLiteral) expr;
+ sb.append("java.util.Map.of(");
+ for (int i = 0; i < mapLit.getEntries().size(); i++) {
+ if (i > 0) {
+ sb.append(", ");
+ }
+ final MALExpressionModel.MapEntry entry = mapLit.getEntries().get(i);
+ sb.append('"').append(escapeJava(entry.getKey())).append("\", ");
+ generateClosureExpr(sb, entry.getValue(), paramName);
+ }
+ sb.append(")");
} else if (expr instanceof MALExpressionModel.ClosureMethodChain) {
generateClosureMethodChain(sb,
(MALExpressionModel.ClosureMethodChain) expr, paramName);
@@ -651,6 +822,24 @@ public final class MALClassGenerator {
}
generateClosureExpr(sb, bin.getRight(), paramName);
sb.append(")");
+ } else if (expr instanceof MALExpressionModel.ClosureTernaryExpr) {
+ final MALExpressionModel.ClosureTernaryExpr ternary =
+ (MALExpressionModel.ClosureTernaryExpr) expr;
+ sb.append("(((Object)(");
+ generateClosureExpr(sb, ternary.getCondition(), paramName);
+ sb.append(")) != null ? (");
+ generateClosureExpr(sb, ternary.getTrueExpr(), paramName);
+ sb.append(") : (");
+ generateClosureExpr(sb, ternary.getFalseExpr(), paramName);
+ sb.append("))");
+ } else if (expr instanceof MALExpressionModel.ClosureElvisExpr) {
+ final MALExpressionModel.ClosureElvisExpr elvis =
+ (MALExpressionModel.ClosureElvisExpr) expr;
+ sb.append("java.util.Optional.ofNullable(");
+ generateClosureExpr(sb, elvis.getPrimary(), paramName);
+ sb.append(").orElse(");
+ generateClosureExpr(sb, elvis.getFallback(), paramName);
+ sb.append(")");
}
}
@@ -660,31 +849,70 @@ public final class MALClassGenerator {
final String paramName) {
// tags.key -> tags.get("key")
// tags['key'] -> tags.get("key")
+ final String target = chain.getTarget();
+ final String resolvedTarget = CLOSURE_CLASS_FQCN.getOrDefault(target, target);
+ final boolean isClassRef = CLOSURE_CLASS_FQCN.containsKey(target);
final List<MALExpressionModel.ClosureChainSegment> segs = chain.getSegments();
+
+ // Static class method call: ProcessRegistry.generateVirtualLocalProcess(...)
+ if (isClassRef) {
+ final StringBuilder local = new StringBuilder();
+ local.append(resolvedTarget);
+ for (final MALExpressionModel.ClosureChainSegment seg : segs) {
+ if (seg instanceof MALExpressionModel.ClosureMethodCallSeg) {
+ final MALExpressionModel.ClosureMethodCallSeg mc =
+ (MALExpressionModel.ClosureMethodCallSeg) seg;
+ local.append('.').append(mc.getName()).append('(');
+ for (int i = 0; i < mc.getArguments().size(); i++) {
+ if (i > 0) {
+ local.append(", ");
+ }
+ generateClosureExpr(local, mc.getArguments().get(i), paramName);
+ }
+ local.append(')');
+ } else if (seg instanceof MALExpressionModel.ClosureFieldAccess) {
+ local.append('.').append(
+ ((MALExpressionModel.ClosureFieldAccess) seg).getName());
+ }
+ }
+ sb.append(local);
+ return;
+ }
+
+ if (segs.isEmpty()) {
+ // Bare identifier (e.g. a local variable like "prefix", "result")
+ sb.append(resolvedTarget);
+ return;
+ }
+
if (segs.size() == 1
&& segs.get(0) instanceof MALExpressionModel.ClosureFieldAccess) {
final String key =
((MALExpressionModel.ClosureFieldAccess) segs.get(0)).getName();
- sb.append(chain.getTarget()).append(".get(\"")
+ sb.append("(String) ").append(resolvedTarget).append(".get(\"")
.append(escapeJava(key)).append("\")");
} else if (segs.size() == 1
&& segs.get(0) instanceof MALExpressionModel.ClosureIndexAccess) {
- sb.append(chain.getTarget()).append(".get(");
+ sb.append("(String) ").append(resolvedTarget).append(".get(");
generateClosureExpr(sb,
((MALExpressionModel.ClosureIndexAccess) segs.get(0)).getIndex(), paramName);
sb.append(")");
} else {
// General chain: build in a local buffer to support safe navigation
final StringBuilder local = new StringBuilder();
- local.append(chain.getTarget());
+ local.append(resolvedTarget);
for (final MALExpressionModel.ClosureChainSegment seg : segs) {
if (seg instanceof MALExpressionModel.ClosureFieldAccess) {
- local.append(".get(\"")
+ final String prior = local.toString();
+ local.setLength(0);
+ local.append("(String) ").append(prior).append(".get(\"")
.append(escapeJava(
((MALExpressionModel.ClosureFieldAccess) seg).getName()))
.append("\")");
} else if (seg instanceof MALExpressionModel.ClosureIndexAccess) {
- local.append(".get(");
+ final String prior2 = local.toString();
+ local.setLength(0);
+ local.append("(String) ").append(prior2).append(".get(");
generateClosureExpr(local,
((MALExpressionModel.ClosureIndexAccess) seg).getIndex(), paramName);
local.append(")");
diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/compiler/MALExpressionModel.java b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/compiler/MALExpressionModel.java
index 455b5db178..d6fda7331c 100644
--- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/compiler/MALExpressionModel.java
+++ b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/compiler/MALExpressionModel.java
@@ -271,13 +271,53 @@ public final class MALExpressionModel {
}
}
+ /**
+ * Assignment statement: {@code tags.key = expr} or {@code tags[expr] = expr}
+ *
+ * <p>{@code mapVar} is the variable (e.g. "tags"), {@code keyExpr} is the key
+ * expression (string literal for field access, or arbitrary expression for bracket access).
+ */
@Getter
public static final class ClosureAssignment implements ClosureStatement {
- private final String target;
+ private final String mapVar;
+ private final ClosureExpr keyExpr;
private final ClosureExpr value;
- public ClosureAssignment(final String target, final ClosureExpr value) {
- this.target = target;
+ public ClosureAssignment(final String mapVar, final ClosureExpr keyExpr,
+ final ClosureExpr value) {
+ this.mapVar = mapVar;
+ this.keyExpr = keyExpr;
+ this.value = value;
+ }
+ }
+
+ /**
+ * Local variable declaration: {@code String result = ""}, {@code String protocol = tags['protocol']}
+ */
+ @Getter
+ public static final class ClosureVarDecl implements ClosureStatement {
+ private final String typeName;
+ private final String varName;
+ private final ClosureExpr initializer;
+
+ public ClosureVarDecl(final String typeName, final String varName,
+ final ClosureExpr initializer) {
+ this.typeName = typeName;
+ this.varName = varName;
+ this.initializer = initializer;
+ }
+ }
+
+ /**
+ * Local variable reassignment: {@code result = '129'}
+ */
+ @Getter
+ public static final class ClosureVarAssign implements ClosureStatement {
+ private final String varName;
+ private final ClosureExpr value;
+
+ public ClosureVarAssign(final String varName, final ClosureExpr value) {
+ this.varName = varName;
this.value = value;
}
}
@@ -326,6 +366,32 @@ public final class MALExpressionModel {
public static final class ClosureNullLiteral implements ClosureExpr {
}
+ /**
+ * Groovy map literal: {@code ['pod': tags.pod, 'namespace': tags.namespace]}
+ */
+ @Getter
+ public static final class ClosureMapLiteral implements ClosureExpr {
+ private final List<MapEntry> entries;
+
+ public ClosureMapLiteral(final List<MapEntry> entries) {
+ this.entries = Collections.unmodifiableList(entries);
+ }
+ }
+
+ /**
+ * Single entry in a Groovy map literal: {@code 'key': expr}
+ */
+ @Getter
+ public static final class MapEntry {
+ private final String key;
+ private final ClosureExpr value;
+
+ public MapEntry(final String key, final ClosureExpr value) {
+ this.key = key;
+ this.value = value;
+ }
+ }
+
/**
* Method chain in closure: {@code tags.service_name}, {@code tags['key']},
* {@code tags.service?.trim()}
@@ -357,6 +423,36 @@ public final class MALExpressionModel {
}
}
+ @Getter
+ public static final class ClosureElvisExpr implements ClosureExpr {
+ private final ClosureExpr primary;
+ private final ClosureExpr fallback;
+
+ public ClosureElvisExpr(final ClosureExpr primary,
+ final ClosureExpr fallback) {
+ this.primary = primary;
+ this.fallback = fallback;
+ }
+ }
+
+ /**
+ * Ternary expression: {@code condition ? trueExpr : falseExpr}
+ */
+ @Getter
+ public static final class ClosureTernaryExpr implements ClosureExpr {
+ private final ClosureExpr condition;
+ private final ClosureExpr trueExpr;
+ private final ClosureExpr falseExpr;
+
+ public ClosureTernaryExpr(final ClosureExpr condition,
+ final ClosureExpr trueExpr,
+ final ClosureExpr falseExpr) {
+ this.condition = condition;
+ this.trueExpr = trueExpr;
+ this.falseExpr = falseExpr;
+ }
+ }
+
// ==================== Closure chain segments ====================
public interface ClosureChainSegment {
diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/compiler/MALScriptParser.java b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/compiler/MALScriptParser.java
index 2c21670387..79a7c83139 100644
--- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/compiler/MALScriptParser.java
+++ b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/compiler/MALScriptParser.java
@@ -51,6 +51,8 @@ import org.apache.skywalking.oap.meter.analyzer.compiler.MALExpressionModel.Clos
import org.apache.skywalking.oap.meter.analyzer.compiler.MALExpressionModel.ClosureReturnStatement;
import org.apache.skywalking.oap.meter.analyzer.compiler.MALExpressionModel.ClosureStatement;
import org.apache.skywalking.oap.meter.analyzer.compiler.MALExpressionModel.ClosureStringLiteral;
+import org.apache.skywalking.oap.meter.analyzer.compiler.MALExpressionModel.ClosureVarAssign;
+import org.apache.skywalking.oap.meter.analyzer.compiler.MALExpressionModel.ClosureVarDecl;
import org.apache.skywalking.oap.meter.analyzer.compiler.MALExpressionModel.CompareOp;
import org.apache.skywalking.oap.meter.analyzer.compiler.MALExpressionModel.EnumRefArgument;
import org.apache.skywalking.oap.meter.analyzer.compiler.MALExpressionModel.Expr;
@@ -302,11 +304,36 @@ public final class MALScriptParser {
? convertClosureExpr(ctx.returnStatement().closureExpr()) : null;
return new ClosureReturnStatement(value);
}
+ if (ctx.variableDeclaration() != null) {
+ final MALParser.VariableDeclarationContext vd = ctx.variableDeclaration();
+ return new ClosureVarDecl(
+ vd.IDENTIFIER(0).getText(),
+ vd.IDENTIFIER(1).getText(),
+ convertClosureExpr(vd.closureExpr()));
+ }
if (ctx.assignmentStatement() != null) {
- final String target = ctx.assignmentStatement().closureFieldAccess().getText();
+ final MALParser.ClosureFieldAccessContext fa =
+ ctx.assignmentStatement().closureFieldAccess();
+ final List<org.antlr.v4.runtime.tree.TerminalNode> ids = fa.IDENTIFIER();
+ final String firstId = ids.get(0).getText();
+ if (ids.size() == 1 && fa.closureExpr() == null) {
+ // bare variable assignment: result = '129'
+ final ClosureExpr value =
+ convertClosureExpr(ctx.assignmentStatement().closureExpr());
+ return new ClosureVarAssign(firstId, value);
+ }
+ // Map assignment: tags.field = value or tags[expr] = value
+ final ClosureExpr keyExpr;
+ if (fa.closureExpr() != null) {
+ // tags[expr] = value
+ keyExpr = convertClosureExpr(fa.closureExpr());
+ } else {
+ // tags.field = value — the key is the last IDENTIFIER
+ keyExpr = new ClosureStringLiteral(ids.get(ids.size() - 1).getText());
+ }
final ClosureExpr value =
convertClosureExpr(ctx.assignmentStatement().closureExpr());
- return new ClosureAssignment(target, value);
+ return new ClosureAssignment(firstId, keyExpr, value);
}
// expressionStatement
return new ClosureExprStatement(
@@ -421,6 +448,21 @@ public final class MALScriptParser {
}
private ClosureExpr convertClosureExpr(final MALParser.ClosureExprContext ctx) {
+ if (ctx instanceof MALParser.ClosureTernaryContext) {
+ final MALParser.ClosureTernaryContext ternary =
+ (MALParser.ClosureTernaryContext) ctx;
+ return new MALExpressionModel.ClosureTernaryExpr(
+ convertClosureExpr(ternary.closureExpr(0)),
+ convertClosureExpr(ternary.closureExpr(1)),
+ convertClosureExpr(ternary.closureExpr(2)));
+ }
+ if (ctx instanceof MALParser.ClosureElvisContext) {
+ final MALParser.ClosureElvisContext elvis =
+ (MALParser.ClosureElvisContext) ctx;
+ return new MALExpressionModel.ClosureElvisExpr(
+ convertClosureExpr(elvis.closureExpr(0)),
+ convertClosureExpr(elvis.closureExpr(1)));
+ }
if (ctx instanceof MALParser.ClosureAddContext) {
final MALParser.ClosureAddContext add = (MALParser.ClosureAddContext) ctx;
return new ClosureBinaryExpr(
@@ -473,6 +515,22 @@ public final class MALScriptParser {
final MALParser.ClosureBoolContext bc = (MALParser.ClosureBoolContext) ctx;
return new ClosureBoolLiteral(bc.boolLiteral().TRUE() != null);
}
+ if (ctx instanceof MALParser.ClosureParenContext) {
+ return convertClosureExpr(
+ ((MALParser.ClosureParenContext) ctx).closureExpr());
+ }
+ if (ctx instanceof MALParser.ClosureMapContext) {
+ final MALParser.ClosureMapLiteralContext mapCtx =
+ ((MALParser.ClosureMapContext) ctx).closureMapLiteral();
+ final List<MALExpressionModel.MapEntry> entries = new ArrayList<>();
+ for (final MALParser.ClosureMapEntryContext entry :
+ mapCtx.closureMapEntry()) {
+ entries.add(new MALExpressionModel.MapEntry(
+ stripQuotes(entry.STRING().getText()),
+ convertClosureExpr(entry.closureExpr())));
+ }
+ return new MALExpressionModel.ClosureMapLiteral(entries);
+ }
// closureChain
final MALParser.ClosureChainContext chain = (MALParser.ClosureChainContext) ctx;
return convertClosureMethodChain(chain.closureMethodChain());
@@ -483,14 +541,16 @@ public final class MALScriptParser {
final String target = ctx.closureTarget().IDENTIFIER().getText();
final List<ClosureChainSegment> segments = new ArrayList<>();
- final int totalSegs = ctx.closureChainSegment().size();
- final int safeNavCount = ctx.safeNav().size();
- final int dotSegCount = totalSegs - safeNavCount;
-
- for (int i = 0; i < totalSegs; i++) {
- final boolean isSafeNav = i >= dotSegCount;
- segments.add(convertClosureChainSegment(
- ctx.closureChainSegment().get(i), isSafeNav));
+ for (final MALParser.ClosureChainAccessContext acc : ctx.closureChainAccess()) {
+ if (acc.closureChainSegment() != null) {
+ final boolean isSafeNav = acc.safeNav() != null;
+ segments.add(convertClosureChainSegment(
+ acc.closureChainSegment(), isSafeNav));
+ } else if (acc.closureExpr() != null) {
+ // Direct bracket access: tags['key'] or tags[expr]
+ segments.add(new ClosureIndexAccess(
+ convertClosureExpr(acc.closureExpr())));
+ }
}
return new ClosureMethodChain(target, segments);
diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/compiler/rt/MalRuntimeHelper.java b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/compiler/rt/MalRuntimeHelper.java
new file mode 100644
index 0000000000..759e6d926c
--- /dev/null
+++ b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/compiler/rt/MalRuntimeHelper.java
@@ -0,0 +1,52 @@
+/*
+ * 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.compiler.rt;
+
+import org.apache.skywalking.oap.meter.analyzer.dsl.Sample;
+import org.apache.skywalking.oap.meter.analyzer.dsl.SampleFamily;
+import org.apache.skywalking.oap.meter.analyzer.dsl.SampleFamilyBuilder;
+
+/**
+ * Static helper methods called by v2-generated {@code MalExpression} classes.
+ * Keeps new runtime behaviour in the v2 compiler package, avoiding modifications
+ * to the shared {@link SampleFamily} class.
+ */
+public final class MalRuntimeHelper {
+
+ private MalRuntimeHelper() {
+ }
+
+ /**
+ * Reverse division: computes {@code numerator / v} for each sample value {@code v}.
+ * Used by generated code for {@code Number / SampleFamily} expressions.
+ */
+ public static SampleFamily divReverse(final double numerator,
+ final SampleFamily sf) {
+ if (sf == SampleFamily.EMPTY) {
+ return SampleFamily.EMPTY;
+ }
+ final Sample[] original = sf.samples;
+ final Sample[] result = new Sample[original.length];
+ for (int i = 0; i < original.length; i++) {
+ result[i] = original[i].toBuilder()
+ .value(numerator / original[i].getValue())
+ .build();
+ }
+ return SampleFamilyBuilder.newBuilder(result).build();
+ }
+}
diff --git a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/compiler/MALClassGeneratorTest.java b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/compiler/MALClassGeneratorTest.java
index 11b96642bb..60496f04f4 100644
--- a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/compiler/MALClassGeneratorTest.java
+++ b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/compiler/MALClassGeneratorTest.java
@@ -127,6 +127,24 @@ class MALClassGeneratorTest {
+ " && tags.Service?.trim() }"));
}
+ @Test
+ void compileValueEqual() throws Exception {
+ final MalExpression expr = generator.compile(
+ "test_value_equal",
+ "kube_node_status_condition.valueEqual(1).sum(['cluster','node','condition'])");
+ assertNotNull(expr);
+ assertNotNull(expr.run(java.util.Map.of()));
+ }
+
+ @Test
+ void compileMethodCallMultiply() throws Exception {
+ final MalExpression expr = generator.compile(
+ "test_multiply",
+ "process_cpu_usage.multiply(100)");
+ assertNotNull(expr);
+ assertNotNull(expr.run(java.util.Map.of()));
+ }
+
// ==================== Error handling tests ====================
@Test
@@ -166,4 +184,178 @@ class MALClassGeneratorTest {
assertThrows(Exception.class,
() -> generator.compileFilter("{ }"));
}
+
+ // ==================== Closure key extraction tests ====================
+
+ @Test
+ void tagClosurePutsCorrectKey() throws Exception {
+ // Issue: tags.cluster = expr should generate tags.put("cluster", ...)
+ // NOT tags.put("tags.cluster", ...)
+ final MalExpression expr = generator.compile(
+ "test_key",
+ "metric.tag({tags -> tags.cluster = 'activemq::' + tags.cluster})");
+ assertNotNull(expr);
+ final String source = generator.generateSource(
+ "metric.tag({tags -> tags.cluster = 'activemq::' + tags.cluster})");
+ assertTrue(source.contains("this._closure0"),
+ "Generated source should reference pre-compiled closure");
+ }
+
+ @Test
+ void tagClosureKeyExtractionViaGeneratedCode() throws Exception {
+ // Verify the closure generates correct put("cluster", ...) not put("tags.cluster", ...)
+ final MalExpression expr = generator.compile(
+ "test_key_gen",
+ "metric.tag({tags -> tags.service_name = 'svc1'})");
+ assertNotNull(expr);
+ assertNotNull(expr.run(java.util.Map.of()));
+ }
+
+ @Test
+ void tagClosureBracketAssignment() throws Exception {
+ // tags['key_name'] = 'value' should also use correct key
+ final MalExpression expr = generator.compile(
+ "test_bracket",
+ "metric.tag({tags -> tags['my_key'] = 'my_value'})");
+ assertNotNull(expr);
+ assertNotNull(expr.run(java.util.Map.of()));
+ }
+
+ // ==================== forEach closure tests ====================
+
+ @Test
+ void forEachClosureCompiles() throws Exception {
+ // forEach requires ForEachFunction.accept(String, Map), not TagFunction.apply(Map)
+ final MalExpression expr = generator.compile(
+ "test_foreach",
+ "metric.forEach(['client', 'server'], {prefix, tags ->"
+ + " tags[prefix + '_name'] = 'value'})");
+ assertNotNull(expr);
+ assertNotNull(expr.run(java.util.Map.of()));
+ }
+
+ @Test
+ void forEachClosureWithBareReturn() throws Exception {
+ // forEach with bare return (void method) — should not throw
+ final MalExpression expr = generator.compile(
+ "test_foreach_return",
+ "metric.forEach(['x'], {prefix, tags ->\n"
+ + " if (tags[prefix + '_id'] != null) {\n"
+ + " return\n"
+ + " }\n"
+ + " tags[prefix + '_id'] = 'default'\n"
+ + "})");
+ assertNotNull(expr);
+ assertNotNull(expr.run(java.util.Map.of()));
+ }
+
+ @Test
+ void forEachClosureWithVarDeclAndElseIf() throws Exception {
+ // Full pattern from network-profiling.yaml second closure
+ final MalExpression expr = generator.compile(
+ "test_foreach_vars",
+ "metric.forEach(['component'], {key, tags ->\n"
+ + " String result = \"\"\n"
+ + " String protocol = tags['protocol']\n"
+ + " String ssl = tags['is_ssl']\n"
+ + " if (protocol == 'http' && ssl == 'true') {\n"
+ + " result = '129'\n"
+ + " } else if (protocol == 'http') {\n"
+ + " result = '49'\n"
+ + " } else if (ssl == 'true') {\n"
+ + " result = '130'\n"
+ + " } else {\n"
+ + " result = '110'\n"
+ + " }\n"
+ + " tags[key] = result\n"
+ + "})");
+ assertNotNull(expr);
+ assertNotNull(expr.run(java.util.Map.of()));
+ }
+
+ // ==================== ProcessRegistry FQCN resolution tests ====================
+
+ @Test
+ void processRegistryResolvedToFQCN() throws Exception {
+ // ProcessRegistry.generateVirtualLocalProcess() should resolve to FQCN
+ final MalExpression expr = generator.compile(
+ "test_registry",
+ "metric.forEach(['client'], {prefix, tags ->\n"
+ + " tags[prefix + '_process_id'] = "
+ + "ProcessRegistry.generateVirtualLocalProcess(tags.service, tags.instance)\n"
+ + "})");
+ assertNotNull(expr);
+ // We can't easily execute this (needs ProcessRegistry runtime) but compile should succeed
+ }
+
+ // ==================== Network-profiling full expression tests ====================
+
+ @Test
+ void networkProfilingFirstClosureCompiles() throws Exception {
+ // Full first closure from network-profiling.yaml expPrefix
+ final MalExpression expr = generator.compile(
+ "test_np1",
+ "metric.forEach(['client', 'server'], { prefix, tags ->\n"
+ + " if (tags[prefix + '_process_id'] != null) {\n"
+ + " return\n"
+ + " }\n"
+ + " if (tags[prefix + '_local'] == 'true') {\n"
+ + " tags[prefix + '_process_id'] = ProcessRegistry"
+ + ".generateVirtualLocalProcess(tags.service, tags.instance)\n"
+ + " return\n"
+ + " }\n"
+ + " tags[prefix + '_process_id'] = ProcessRegistry"
+ + ".generateVirtualRemoteProcess(tags.service, tags.instance,"
+ + " tags[prefix + '_address'])\n"
+ + " })");
+ assertNotNull(expr);
+ }
+
+ @Test
+ void networkProfilingSecondClosureCompiles() throws Exception {
+ // Full second closure from network-profiling.yaml expPrefix
+ final MalExpression expr = generator.compile(
+ "test_np2",
+ "metric.forEach(['component'], { key, tags ->\n"
+ + " String result = \"\"\n"
+ + " // protocol are defined in the component-libraries.yml\n"
+ + " String protocol = tags['protocol']\n"
+ + " String ssl = tags['is_ssl']\n"
+ + " if (protocol == 'http' && ssl == 'true') {\n"
+ + " result = '129'\n"
+ + " } else if (protocol == 'http') {\n"
+ + " result = '49'\n"
+ + " } else if (ssl == 'true') {\n"
+ + " result = '130'\n"
+ + " } else {\n"
+ + " result = '110'\n"
+ + " }\n"
+ + " tags[key] = result\n"
+ + " })");
+ assertNotNull(expr);
+ }
+
+ // ==================== String concatenation in closures ====================
+
+ @Test
+ void apisixExpressionCompiles() throws Exception {
+ // The APISIX expression that originally triggered the E2E failure:
+ // safe navigation + elvis + bracket access + string concat
+ final MalExpression expr = generator.compile(
+ "test_apisix",
+ "metric.tag({tags -> tags.service_name = 'APISIX::'"
+ + "+(tags['skywalking_service']?.trim()?:'APISIX')})");
+ assertNotNull(expr);
+ assertNotNull(expr.run(java.util.Map.of()));
+ }
+
+ @Test
+ void closureStringConcatenation() throws Exception {
+ // APISIX-style: tags.service_name = 'APISIX::' + tags.service
+ final MalExpression expr = generator.compile(
+ "test_concat",
+ "metric.tag({tags -> tags.service_name = 'APISIX::' + tags.service})");
+ assertNotNull(expr);
+ assertNotNull(expr.run(java.util.Map.of()));
+ }
}
diff --git a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/compiler/MALScriptParserTest.java b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/compiler/MALScriptParserTest.java
index 40f50f5745..eeb71518c7 100644
--- a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/compiler/MALScriptParserTest.java
+++ b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/compiler/MALScriptParserTest.java
@@ -230,6 +230,127 @@ class MALScriptParserTest {
assertEquals("Pod2Service", enumArg.getEnumValue());
}
+ @Test
+ void parseTagAssignmentExtractsCorrectKey() {
+ // Issue: tags.cluster = expr should produce key "cluster", not "tags.cluster"
+ final MALExpressionModel.Expr ast = MALScriptParser.parse(
+ "metric.tag({tags -> tags.cluster = 'activemq::' + tags.cluster})");
+ assertInstanceOf(MetricExpr.class, ast);
+ final MetricExpr metric = (MetricExpr) ast;
+ final ClosureArgument closure =
+ (ClosureArgument) metric.getMethodChain().get(0).getArguments().get(0);
+ assertEquals(1, closure.getBody().size());
+
+ final MALExpressionModel.ClosureAssignment assign =
+ (MALExpressionModel.ClosureAssignment) closure.getBody().get(0);
+ assertEquals("tags", assign.getMapVar());
+ // Key should be "cluster", not "tags.cluster"
+ assertInstanceOf(MALExpressionModel.ClosureStringLiteral.class, assign.getKeyExpr());
+ assertEquals("cluster",
+ ((MALExpressionModel.ClosureStringLiteral) assign.getKeyExpr()).getValue());
+ }
+
+ @Test
+ void parseTagBracketAssignment() {
+ // tags[prefix + '_process_id'] = expr
+ final MALExpressionModel.Expr ast = MALScriptParser.parse(
+ "metric.tag({prefix, tags -> tags[prefix + '_id'] = 'val'})");
+ assertInstanceOf(MetricExpr.class, ast);
+ final MetricExpr metric = (MetricExpr) ast;
+ final ClosureArgument closure =
+ (ClosureArgument) metric.getMethodChain().get(0).getArguments().get(0);
+ assertEquals(List.of("prefix", "tags"), closure.getParams());
+
+ final MALExpressionModel.ClosureAssignment assign =
+ (MALExpressionModel.ClosureAssignment) closure.getBody().get(0);
+ assertEquals("tags", assign.getMapVar());
+ // Key is a binary expression (prefix + '_id')
+ assertInstanceOf(MALExpressionModel.ClosureBinaryExpr.class, assign.getKeyExpr());
+ }
+
+ @Test
+ void parseForEachClosure() {
+ final MALExpressionModel.Expr ast = MALScriptParser.parse(
+ "metric.forEach(['client', 'server'], {prefix, tags -> tags.key = prefix})");
+ assertInstanceOf(MetricExpr.class, ast);
+ final MetricExpr metric = (MetricExpr) ast;
+ assertEquals("forEach", metric.getMethodChain().get(0).getName());
+
+ final ClosureArgument closure =
+ (ClosureArgument) metric.getMethodChain().get(0).getArguments().get(1);
+ assertEquals(List.of("prefix", "tags"), closure.getParams());
+ }
+
+ @Test
+ void parseVariableDeclaration() {
+ // String result = "" — Groovy local variable declaration
+ final MALExpressionModel.Expr ast = MALScriptParser.parse(
+ "metric.forEach(['x'], {key, tags ->\n"
+ + " String result = \"\"\n"
+ + " tags[key] = result\n"
+ + "})");
+ assertInstanceOf(MetricExpr.class, ast);
+ final MetricExpr metric = (MetricExpr) ast;
+ final ClosureArgument closure =
+ (ClosureArgument) metric.getMethodChain().get(0).getArguments().get(1);
+ assertEquals(2, closure.getBody().size());
+ // First statement: variable declaration
+ assertInstanceOf(MALExpressionModel.ClosureVarDecl.class, closure.getBody().get(0));
+ final MALExpressionModel.ClosureVarDecl vd =
+ (MALExpressionModel.ClosureVarDecl) closure.getBody().get(0);
+ assertEquals("String", vd.getTypeName());
+ assertEquals("result", vd.getVarName());
+ }
+
+ @Test
+ void parseBareReturn() {
+ // return with no expression (void return)
+ final MALExpressionModel.Expr ast = MALScriptParser.parse(
+ "metric.forEach(['x'], {prefix, tags ->\n"
+ + " if (tags[prefix + '_id'] != null) {\n"
+ + " return\n"
+ + " }\n"
+ + " tags[prefix + '_id'] = 'default'\n"
+ + "})");
+ assertInstanceOf(MetricExpr.class, ast);
+ final MetricExpr metric = (MetricExpr) ast;
+ final ClosureArgument closure =
+ (ClosureArgument) metric.getMethodChain().get(0).getArguments().get(1);
+ // First statement is if, which contains a bare return
+ assertInstanceOf(MALExpressionModel.ClosureIfStatement.class, closure.getBody().get(0));
+ final MALExpressionModel.ClosureIfStatement ifStmt =
+ (MALExpressionModel.ClosureIfStatement) closure.getBody().get(0);
+ assertInstanceOf(MALExpressionModel.ClosureReturnStatement.class,
+ ifStmt.getThenBranch().get(0));
+ final MALExpressionModel.ClosureReturnStatement ret =
+ (MALExpressionModel.ClosureReturnStatement) ifStmt.getThenBranch().get(0);
+ // Bare return — value should be null
+ assertEquals(null, ret.getValue());
+ }
+
+ @Test
+ void parseStaticMethodCall() {
+ // ProcessRegistry.generateVirtualLocalProcess(tags.service, tags.instance)
+ final MALExpressionModel.Expr ast = MALScriptParser.parse(
+ "metric.tag({tags -> "
+ + "tags.pid = ProcessRegistry.generateVirtualLocalProcess(tags.service, tags.instance)"
+ + "})");
+ assertInstanceOf(MetricExpr.class, ast);
+ final MetricExpr metric = (MetricExpr) ast;
+ final ClosureArgument closure =
+ (ClosureArgument) metric.getMethodChain().get(0).getArguments().get(0);
+ final MALExpressionModel.ClosureAssignment assign =
+ (MALExpressionModel.ClosureAssignment) closure.getBody().get(0);
+ // RHS should be a ClosureMethodChain with target "ProcessRegistry"
+ assertInstanceOf(MALExpressionModel.ClosureMethodChain.class, assign.getValue());
+ final MALExpressionModel.ClosureMethodChain chain =
+ (MALExpressionModel.ClosureMethodChain) assign.getValue();
+ assertEquals("ProcessRegistry", chain.getTarget());
+ assertEquals(1, chain.getSegments().size());
+ assertInstanceOf(MALExpressionModel.ClosureMethodCallSeg.class,
+ chain.getSegments().get(0));
+ }
+
@Test
void parseSyntaxErrorThrows() {
assertThrows(IllegalArgumentException.class,
diff --git a/oap-server/server-starter/pom.xml b/oap-server/server-starter/pom.xml
index 35b5e7de81..109093dcdf 100644
--- a/oap-server/server-starter/pom.xml
+++ b/oap-server/server-starter/pom.xml
@@ -47,6 +47,13 @@
</dependency>
<!-- OAL runtime core -->
+ <!-- Hierarchy rule compiler (SPI-loaded by server-core) -->
+ <dependency>
+ <groupId>org.apache.skywalking</groupId>
+ <artifactId>hierarchy</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
<!-- cluster module -->
<dependency>
<groupId>org.apache.skywalking</groupId>
diff --git a/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalComparisonTest.java b/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalComparisonTest.java
index e69c2185ac..7b70844715 100644
--- a/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalComparisonTest.java
+++ b/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalComparisonTest.java
@@ -19,7 +19,6 @@ package org.apache.skywalking.oap.server.checker.lal;
import java.io.File;
import java.lang.reflect.Field;
-import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
@@ -62,7 +61,7 @@ import static org.mockito.Mockito.when;
* <ul>
* <li>Path A (v1): Groovy compilation + runtime execution via {@link DSL}</li>
* <li>Path B (v2): ANTLR4 + Javassist compilation via {@link LALClassGenerator}
- * + runtime execution via reflective {@code execute(Object, Object)} call</li>
+ * + runtime execution via {@code LalExpression.execute(FilterSpec, Binding)}</li>
* </ul>
* Both paths are fed the same mock LogData and the resulting Binding state
* (service, layer, tags, abort/save flags) is compared.
@@ -110,7 +109,8 @@ class LalComparisonTest {
String v2Error = null;
try {
final LALClassGenerator generator = new LALClassGenerator();
- final Object v2Expr = generator.compile(dsl);
+ final org.apache.skywalking.oap.log.analyzer.dsl.LalExpression v2Expr =
+ generator.compile(dsl);
final FilterSpec v2FilterSpec = new FilterSpec(manager, new LogAnalyzerModuleConfig());
disableSinkListenersOnSpec(v2FilterSpec);
@@ -118,11 +118,7 @@ class LalComparisonTest {
v2Binding = new Binding().log(testLog);
v2FilterSpec.bind(v2Binding);
- // Call execute(Object, Object) via reflection since the Javassist-generated
- // class declares execute(Object, Object) rather than the typed signature
- final Method executeMethod = v2Expr.getClass().getMethod("execute",
- Object.class, Object.class);
- executeMethod.invoke(v2Expr, v2FilterSpec, v2Binding);
+ v2Expr.execute(v2FilterSpec, v2Binding);
} catch (Exception e) {
final Throwable cause = e.getCause() != null ? e.getCause() : e;
v2Error = cause.getClass().getSimpleName() + ": " + cause.getMessage();
@@ -286,14 +282,11 @@ class LalComparisonTest {
return result;
}
- final File[] files = lalDir.toFile().listFiles();
- if (files == null) {
- return result;
- }
- for (final File file : files) {
- if (!file.getName().endsWith(".yaml") && !file.getName().endsWith(".yml")) {
- continue;
- }
+ // Scan top-level and subdirectories (oap-cases/, feature-cases/)
+ final List<File> yamlFiles = new ArrayList<>();
+ collectYamlFiles(lalDir.toFile(), yamlFiles);
+
+ for (final File file : yamlFiles) {
final String content = Files.readString(file.toPath());
final Map<String, Object> config = yaml.load(content);
if (config == null || !config.containsKey("rules")) {
@@ -314,7 +307,8 @@ class LalComparisonTest {
lalRules.add(new LalRule(name, dslStr));
}
if (!lalRules.isEmpty()) {
- result.put("lal/" + file.getName(), lalRules);
+ final String relative = lalDir.relativize(file.toPath()).toString();
+ result.put("lal/" + relative, lalRules);
}
}
return result;
@@ -334,6 +328,22 @@ class LalComparisonTest {
return null;
}
+ private static void collectYamlFiles(final File dir,
+ final List<File> result) {
+ final File[] children = dir.listFiles();
+ if (children == null) {
+ return;
+ }
+ for (final File child : children) {
+ if (child.isDirectory()) {
+ collectYamlFiles(child, result);
+ } else if (child.getName().endsWith(".yaml")
+ || child.getName().endsWith(".yml")) {
+ result.add(child);
+ }
+ }
+ }
+
private static class LalRule {
final String name;
final String dsl;
diff --git a/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalComparisonTest.java b/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalComparisonTest.java
index 6784e3852a..991dd0e317 100644
--- a/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalComparisonTest.java
+++ b/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalComparisonTest.java
@@ -21,10 +21,14 @@ import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import com.google.common.collect.ImmutableMap;
import lombok.extern.slf4j.Slf4j;
import org.apache.skywalking.oap.meter.analyzer.compiler.MALClassGenerator;
import org.apache.skywalking.oap.meter.analyzer.dsl.DSL;
@@ -32,6 +36,10 @@ import org.apache.skywalking.oap.meter.analyzer.dsl.Expression;
import org.apache.skywalking.oap.meter.analyzer.dsl.ExpressionMetadata;
import org.apache.skywalking.oap.meter.analyzer.dsl.ExpressionParsingContext;
import org.apache.skywalking.oap.meter.analyzer.dsl.MalExpression;
+import org.apache.skywalking.oap.meter.analyzer.dsl.Result;
+import org.apache.skywalking.oap.meter.analyzer.dsl.Sample;
+import org.apache.skywalking.oap.meter.analyzer.dsl.SampleFamily;
+import org.apache.skywalking.oap.meter.analyzer.dsl.SampleFamilyBuilder;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import org.yaml.snakeyaml.Yaml;
@@ -46,12 +54,21 @@ import static org.junit.jupiter.api.Assertions.fail;
* <li>Path A (v1): Groovy compilation via upstream {@code DSL.parse()}</li>
* <li>Path B (v2): ANTLR4 + Javassist compilation via {@link MALClassGenerator}</li>
* </ul>
- * Both paths run metadata extraction and compare the resulting metadata
- * (samples, scope, downsampling, aggregation labels).
+ * Both paths run metadata extraction and runtime execution comparison.
+ * Metadata: compare samples, scope, downsampling, aggregation labels.
+ * Runtime: execute with mock SampleFamily data and compare output.
*/
@Slf4j
class MalComparisonTest {
+ private static final Pattern TAG_EQUAL_PATTERN =
+ Pattern.compile("\\.tagEqual\\s*\\(\\s*'([^']+)'\\s*,\\s*'([^']+)'\\s*\\)");
+
+ private static final String[] HISTOGRAM_LE_VALUES =
+ {"50", "100", "250", "500", "1000"};
+
+ private long timestampCounter = System.currentTimeMillis();
+
@TestFactory
Collection<DynamicTest> malExpressionsMatch() throws Exception {
final List<DynamicTest> tests = new ArrayList<>();
@@ -73,26 +90,28 @@ class MalComparisonTest {
private void compareExpression(final String metricName,
final String expression) throws Exception {
// ---- V1: Groovy path ----
+ Expression v1Expr = null;
ExpressionParsingContext v1Ctx = null;
try {
- final Expression v1Expr = DSL.parse(metricName, expression);
+ v1Expr = DSL.parse(metricName, expression);
v1Ctx = v1Expr.parse();
} catch (Exception e) {
// V1 failed - skip comparison
}
// ---- V2: ANTLR4 + Javassist compilation ----
+ MalExpression v2MalExpr = null;
ExpressionMetadata v2Meta = null;
String v2Error = null;
try {
final MALClassGenerator generator = new MALClassGenerator();
- final MalExpression malExpr = generator.compile(metricName, expression);
- v2Meta = malExpr.metadata();
+ v2MalExpr = generator.compile(metricName, expression);
+ v2Meta = v2MalExpr.metadata();
} catch (Exception e) {
v2Error = e.getMessage();
}
- // ---- Compare ----
+ // ---- Compare metadata ----
if (v1Ctx == null && v2Meta == null) {
// Both failed — consistent behavior
return;
@@ -103,6 +122,7 @@ class MalComparisonTest {
}
if (v2Meta == null) {
fail(metricName + ": v2 compile failed but v1 succeeded — " + v2Error);
+ return;
}
// Both succeeded - compare metadata
@@ -118,6 +138,210 @@ class MalComparisonTest {
metricName + ": scopeLabels mismatch");
assertEquals(v1Ctx.getAggregationLabels(), v2Meta.getAggregationLabels(),
metricName + ": aggregationLabels mismatch");
+
+ // ---- Runtime execution comparison ----
+ compareExecution(metricName, expression, v1Expr, v2MalExpr, v2Meta);
+ }
+
+ private void compareExecution(final String metricName,
+ final String expression,
+ final Expression v1Expr,
+ final MalExpression v2MalExpr,
+ final ExpressionMetadata v2Meta) {
+ final boolean hasIncrease = expression.contains(".increase(")
+ || expression.contains(".rate(");
+
+ // For increase()/rate(), prime the CounterWindow with initial data
+ if (hasIncrease) {
+ try {
+ v1Expr.run(buildMockData(metricName, expression, v2Meta));
+ } catch (Exception ignored) {
+ }
+ try {
+ final Map<String, SampleFamily> primeData =
+ buildMockData(metricName, expression, v2Meta);
+ for (final SampleFamily s : primeData.values()) {
+ if (s != SampleFamily.EMPTY) {
+ s.context.setMetricName(metricName);
+ }
+ }
+ v2MalExpr.run(primeData);
+ } catch (Exception ignored) {
+ }
+ }
+
+ // Build fresh test data for actual comparison
+ final Map<String, SampleFamily> v1Data =
+ buildMockData(metricName, expression, v2Meta);
+ final Map<String, SampleFamily> v2Data =
+ buildMockData(metricName, expression, v2Meta);
+
+ // V1 run
+ Result v1Result;
+ try {
+ v1Result = v1Expr.run(v1Data);
+ } catch (Exception e) {
+ // V1 runtime failed — skip comparison
+ return;
+ }
+
+ // V2 run
+ SampleFamily v2Sf;
+ try {
+ for (final SampleFamily s : v2Data.values()) {
+ if (s != SampleFamily.EMPTY) {
+ s.context.setMetricName(metricName);
+ }
+ }
+ v2Sf = v2MalExpr.run(v2Data);
+ } catch (Exception e) {
+ if (v1Result.isSuccess()) {
+ fail(metricName + ": v2 runtime failed but v1 succeeded — "
+ + e.getClass().getSimpleName() + ": " + e.getMessage());
+ }
+ return;
+ }
+
+ // Compare results
+ final boolean v2Success = v2Sf != null && v2Sf != SampleFamily.EMPTY;
+ assertEquals(v1Result.isSuccess(), v2Success,
+ metricName + ": success mismatch (v1=" + v1Result.isSuccess()
+ + ", v2=" + v2Success + ")");
+
+ if (v1Result.isSuccess() && v2Success) {
+ compareSampleFamilies(metricName, v1Result.getData(), v2Sf);
+ }
+ }
+
+ private Map<String, SampleFamily> buildMockData(final String metricName,
+ final String expression,
+ final ExpressionMetadata meta) {
+ final Map<String, SampleFamily> data = new HashMap<>();
+ final long now = timestampCounter++;
+
+ // Extract tagEqual constraints from the expression
+ final Map<String, String> tagEqualLabels = extractTagEqualLabels(expression);
+
+ for (final String sampleName : meta.getSamples()) {
+ // Build labels from aggregation labels
+ final Map<String, String> labels = new HashMap<>();
+ for (final String label : meta.getAggregationLabels()) {
+ labels.put(label, inferLabelValue(label, tagEqualLabels));
+ }
+ // Also add tagEqual labels not in aggregation labels
+ labels.putAll(tagEqualLabels);
+
+ if (meta.isHistogram()) {
+ data.put(sampleName,
+ buildHistogramSamples(sampleName, labels, now));
+ } else {
+ final Sample sample = Sample.builder()
+ .name(sampleName)
+ .labels(ImmutableMap.copyOf(labels))
+ .value(100.0)
+ .timestamp(now)
+ .build();
+ data.put(sampleName,
+ SampleFamilyBuilder.newBuilder(sample).build());
+ }
+ }
+ return data;
+ }
+
+ private SampleFamily buildHistogramSamples(final String sampleName,
+ final Map<String, String> baseLabels,
+ final long timestamp) {
+ final List<Sample> samples = new ArrayList<>();
+ double cumulativeValue = 0;
+ for (final String le : HISTOGRAM_LE_VALUES) {
+ cumulativeValue += 10.0;
+ final Map<String, String> labels = new HashMap<>(baseLabels);
+ labels.put("le", le);
+ samples.add(Sample.builder()
+ .name(sampleName)
+ .labels(ImmutableMap.copyOf(labels))
+ .value(cumulativeValue)
+ .timestamp(timestamp)
+ .build());
+ }
+ return SampleFamilyBuilder.newBuilder(
+ samples.toArray(new Sample[0])).build();
+ }
+
+ private static Map<String, String> extractTagEqualLabels(final String expression) {
+ final Map<String, String> labels = new HashMap<>();
+ final Matcher matcher = TAG_EQUAL_PATTERN.matcher(expression);
+ while (matcher.find()) {
+ labels.put(matcher.group(1), matcher.group(2));
+ }
+ return labels;
+ }
+
+ private static String inferLabelValue(final String label,
+ final Map<String, String> tagEqualLabels) {
+ // If tagEqual specifies this label, use that value
+ if (tagEqualLabels.containsKey(label)) {
+ return tagEqualLabels.get(label);
+ }
+ switch (label) {
+ case "service":
+ return "test-service";
+ case "instance":
+ case "service_instance_id":
+ return "test-instance";
+ case "endpoint":
+ return "/test";
+ case "host_name":
+ return "test-host";
+ case "le":
+ return "100";
+ case "job_name":
+ return "mysql-monitoring";
+ case "cluster":
+ return "test-cluster";
+ case "node":
+ case "node_id":
+ return "test-node";
+ case "topic":
+ return "test-topic";
+ case "queue":
+ return "test-queue";
+ case "broker":
+ return "test-broker";
+ default:
+ return "test-value";
+ }
+ }
+
+ private static void compareSampleFamilies(final String metricName,
+ final SampleFamily v1Sf,
+ final SampleFamily v2Sf) {
+ // Sort both sample arrays by labels for stable comparison
+ final Sample[] v1Sorted = sortSamples(v1Sf.samples);
+ final Sample[] v2Sorted = sortSamples(v2Sf.samples);
+
+ assertEquals(v1Sorted.length, v2Sorted.length,
+ metricName + ": output sample count mismatch (v1="
+ + v1Sorted.length + ", v2=" + v2Sorted.length + ")");
+
+ for (int i = 0; i < v1Sorted.length; i++) {
+ assertEquals(v1Sorted[i].getLabels(), v2Sorted[i].getLabels(),
+ metricName + ": output sample[" + i + "] labels mismatch");
+ assertEquals(v1Sorted[i].getValue(), v2Sorted[i].getValue(), 0.001,
+ metricName + ": output sample[" + i + "] value mismatch"
+ + " (v1=" + v1Sorted[i].getValue()
+ + ", v2=" + v2Sorted[i].getValue() + ")");
+ }
+ }
+
+ private static Sample[] sortSamples(final Sample[] samples) {
+ final Sample[] sorted = Arrays.copyOf(samples, samples.length);
+ Arrays.sort(sorted, (a, b) -> {
+ final String aKey = a.getLabels().toString();
+ final String bKey = b.getLabels().toString();
+ return aKey.compareTo(bKey);
+ });
+ return sorted;
}
@SuppressWarnings("unchecked")
diff --git a/test/script-cases/scripts/lal/test-lal/feature-cases/execution-basic.input.data b/test/script-cases/scripts/lal/test-lal/feature-cases/execution-basic.input.data
new file mode 100644
index 0000000000..de10ed6543
--- /dev/null
+++ b/test/script-cases/scripts/lal/test-lal/feature-cases/execution-basic.input.data
@@ -0,0 +1,157 @@
+# 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.
+
+# Mock input data and expected output for execution-basic.yaml rules.
+# Format: YAML keyed by rule name. Each entry describes body-type, body,
+# optional tags, and expected assertions after execution.
+
+json-parse-extract:
+ body-type: json
+ body: '{"service":"my-svc","instance":"inst-01","endpoint":"/api","layer":"GENERAL"}'
+ expect:
+ service: my-svc
+ instance: inst-01
+ endpoint: /api
+ layer: GENERAL
+ save: true
+ abort: false
+
+tag-condition-true:
+ body-type: json
+ body: '{"service":"db-svc"}'
+ tags:
+ LOG_KIND: SLOW_SQL
+ expect:
+ service: db-svc
+
+tag-condition-false:
+ body-type: json
+ body: '{"service":"db-svc"}'
+ tags:
+ LOG_KIND: NORMAL
+ expect:
+ service: ""
+
+tag-assignment:
+ body-type: json
+ body: '{"env":"prod","region":"us-east"}'
+ expect:
+ tag.key1: prod
+ tag.key2: us-east
+
+safe-nav-missing:
+ body-type: json
+ body: '{"other":"val"}'
+ expect:
+ service: ""
+ abort: false
+
+safe-nav-present:
+ body-type: json
+ body: '{"data":{"name":"found"}}'
+ expect:
+ service: found
+
+if-else-if-error:
+ body-type: json
+ body: '{"level":"ERROR"}'
+ expect:
+ service: error-handler
+
+if-else-if-warn:
+ body-type: json
+ body: '{"level":"WARN"}'
+ expect:
+ service: warn-handler
+
+if-else-if-default:
+ body-type: json
+ body: '{"level":"DEBUG"}'
+ expect:
+ service: default-handler
+
+sink-enforcer:
+ body-type: json
+ body: '{}'
+ expect:
+ save: true
+
+sink-dropper:
+ body-type: json
+ body: '{}'
+ expect:
+ save: false
+
+sampler-rate-limit:
+ body-type: json
+ body: '{}'
+ expect:
+ save: true
+
+sampler-interpolated-id:
+ body-type: json
+ body: '{"code":"200"}'
+ expect:
+ save: true
+
+abort-stops-pipeline:
+ body-type: json
+ body: '{"service":"should-not-be-set"}'
+ expect:
+ abort: true
+ service: ""
+
+conditional-abort-true:
+ body-type: json
+ body: '{"skip":"true","service":"my-svc"}'
+ expect:
+ abort: true
+ service: ""
+
+conditional-abort-false:
+ body-type: json
+ body: '{"skip":"false","service":"my-svc"}'
+ expect:
+ abort: false
+ service: my-svc
+
+timestamp-extraction:
+ body-type: json
+ body: '{"time":"1609459200000"}'
+ expect:
+ timestamp: 1609459200000
+
+text-parser-regexp:
+ body-type: text
+ body: "1609459200000 ERROR Something failed"
+ expect:
+ service: ERROR
+
+sampled-trace-basic:
+ body-type: json
+ body: '{"latency":150,"uri":"/test","reason":"slow","pid":"proc-a","dpid":"proc-b","dp":"client"}'
+ service: trace-svc
+ instance: trace-inst
+ trace-id: trace-basic-001
+ timestamp: 1609459200000
+ expect:
+ save: true
+ sampledTrace.latency: 150
+ sampledTrace.uri: /test
+ sampledTrace.reason: SLOW
+ sampledTrace.processId: proc-a
+ sampledTrace.destProcessId: proc-b
+ sampledTrace.detectPoint: CLIENT
+ sampledTrace.componentId: 49
diff --git a/test/script-cases/scripts/lal/test-lal/feature-cases/execution-basic.yaml b/test/script-cases/scripts/lal/test-lal/feature-cases/execution-basic.yaml
new file mode 100644
index 0000000000..71e3bb7d40
--- /dev/null
+++ b/test/script-cases/scripts/lal/test-lal/feature-cases/execution-basic.yaml
@@ -0,0 +1,272 @@
+# 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.
+
+# Feature-focused execution test rules for LAL v2 compiler.
+# Each rule exercises a specific LAL language feature.
+# Paired with execution-basic.input.data for mock input and expected output.
+rules:
+ - name: json-parse-extract
+ layer: GENERAL
+ dsl: |
+ filter {
+ json {}
+ extractor {
+ service parsed.service as String
+ instance parsed.instance as String
+ endpoint parsed.endpoint as String
+ layer parsed.layer as String
+ }
+ sink {}
+ }
+
+ - name: tag-condition-true
+ layer: GENERAL
+ dsl: |
+ filter {
+ json {}
+ if (tag("LOG_KIND") == "SLOW_SQL") {
+ extractor {
+ service parsed.service as String
+ }
+ sink {}
+ }
+ }
+
+ - name: tag-condition-false
+ layer: GENERAL
+ dsl: |
+ filter {
+ json {}
+ if (tag("LOG_KIND") == "SLOW_SQL") {
+ extractor {
+ service parsed.service as String
+ }
+ sink {}
+ }
+ }
+
+ - name: tag-assignment
+ layer: GENERAL
+ dsl: |
+ filter {
+ json {}
+ extractor {
+ tag key1: parsed.env as String, key2: parsed.region as String
+ }
+ sink {}
+ }
+
+ - name: safe-nav-missing
+ layer: GENERAL
+ dsl: |
+ filter {
+ json {}
+ extractor {
+ service parsed?.missing?.deep as String
+ }
+ sink {}
+ }
+
+ - name: safe-nav-present
+ layer: GENERAL
+ dsl: |
+ filter {
+ json {}
+ extractor {
+ service parsed?.data?.name as String
+ }
+ sink {}
+ }
+
+ - name: if-else-if-error
+ layer: GENERAL
+ dsl: |
+ filter {
+ json {}
+ if (parsed.level == "ERROR") {
+ extractor { service "error-handler" as String }
+ sink {}
+ } else if (parsed.level == "WARN") {
+ extractor { service "warn-handler" as String }
+ sink {}
+ } else {
+ extractor { service "default-handler" as String }
+ sink {}
+ }
+ }
+
+ - name: if-else-if-warn
+ layer: GENERAL
+ dsl: |
+ filter {
+ json {}
+ if (parsed.level == "ERROR") {
+ extractor { service "error-handler" as String }
+ sink {}
+ } else if (parsed.level == "WARN") {
+ extractor { service "warn-handler" as String }
+ sink {}
+ } else {
+ extractor { service "default-handler" as String }
+ sink {}
+ }
+ }
+
+ - name: if-else-if-default
+ layer: GENERAL
+ dsl: |
+ filter {
+ json {}
+ if (parsed.level == "ERROR") {
+ extractor { service "error-handler" as String }
+ sink {}
+ } else if (parsed.level == "WARN") {
+ extractor { service "warn-handler" as String }
+ sink {}
+ } else {
+ extractor { service "default-handler" as String }
+ sink {}
+ }
+ }
+
+ - name: sink-enforcer
+ layer: GENERAL
+ dsl: |
+ filter {
+ json {}
+ sink {
+ enforcer {}
+ }
+ }
+
+ - name: sink-dropper
+ layer: GENERAL
+ dsl: |
+ filter {
+ json {}
+ sink {
+ dropper {}
+ }
+ }
+
+ - name: sampler-rate-limit
+ layer: GENERAL
+ dsl: |
+ filter {
+ json {}
+ sink {
+ sampler {
+ rateLimit('test:svc') {
+ rpm 6000
+ }
+ }
+ }
+ }
+
+ - name: sampler-interpolated-id
+ layer: GENERAL
+ dsl: |
+ filter {
+ json {}
+ sink {
+ sampler {
+ rateLimit("${parsed.code}") {
+ rpm 6000
+ }
+ }
+ }
+ }
+
+ - name: abort-stops-pipeline
+ layer: GENERAL
+ dsl: |
+ filter {
+ json {}
+ abort {}
+ extractor {
+ service parsed.service as String
+ }
+ sink {}
+ }
+
+ - name: conditional-abort-true
+ layer: GENERAL
+ dsl: |
+ filter {
+ json {}
+ if (parsed.skip == "true") {
+ abort {}
+ }
+ extractor {
+ service parsed.service as String
+ }
+ sink {}
+ }
+
+ - name: conditional-abort-false
+ layer: GENERAL
+ dsl: |
+ filter {
+ json {}
+ if (parsed.skip == "true") {
+ abort {}
+ }
+ extractor {
+ service parsed.service as String
+ }
+ sink {}
+ }
+
+ - name: timestamp-extraction
+ layer: GENERAL
+ dsl: |
+ filter {
+ json {}
+ extractor {
+ timestamp parsed.time as String
+ }
+ sink {}
+ }
+
+ - name: text-parser-regexp
+ layer: GENERAL
+ dsl: |
+ filter {
+ text {
+ regexp $/(?<ts>\d+) (?<lvl>\w+) (?<msg>.*)/$
+ }
+ extractor {
+ service parsed.lvl as String
+ }
+ sink {}
+ }
+
+ - name: sampled-trace-basic
+ layer: MESH_DP
+ dsl: |
+ filter {
+ json {}
+ extractor {
+ sampledTrace {
+ latency parsed.latency as Long
+ uri parsed.uri as String
+ reason parsed.reason as String
+ processId parsed.pid as String
+ destProcessId parsed.dpid as String
+ detectPoint parsed.dp as String
+ componentId 49
+ }
+ }
+ }
diff --git a/test/script-cases/scripts/lal/test-lal/default.yaml b/test/script-cases/scripts/lal/test-lal/oap-cases/default.input.data
similarity index 80%
copy from test/script-cases/scripts/lal/test-lal/default.yaml
copy to test/script-cases/scripts/lal/test-lal/oap-cases/default.input.data
index 12317a95bf..6823207fb0 100644
--- a/test/script-cases/scripts/lal/test-lal/default.yaml
+++ b/test/script-cases/scripts/lal/test-lal/oap-cases/default.input.data
@@ -13,12 +13,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-# The default LAL script to save all logs, behaving like the versions before 8.5.0.
-rules:
- - name: default
- layer: GENERAL
- dsl: |
- filter {
- sink {
- }
- }
+# Mock input data for default.yaml rules.
+
+default:
+ body-type: json
+ body: '{}'
+ expect:
+ save: true
+ abort: false
diff --git a/test/script-cases/scripts/lal/test-lal/default.yaml b/test/script-cases/scripts/lal/test-lal/oap-cases/default.yaml
similarity index 100%
copy from test/script-cases/scripts/lal/test-lal/default.yaml
copy to test/script-cases/scripts/lal/test-lal/oap-cases/default.yaml
diff --git a/test/script-cases/scripts/lal/test-lal/default.yaml b/test/script-cases/scripts/lal/test-lal/oap-cases/envoy-als.input.data
similarity index 61%
copy from test/script-cases/scripts/lal/test-lal/default.yaml
copy to test/script-cases/scripts/lal/test-lal/oap-cases/envoy-als.input.data
index 12317a95bf..948864414d 100644
--- a/test/script-cases/scripts/lal/test-lal/default.yaml
+++ b/test/script-cases/scripts/lal/test-lal/oap-cases/envoy-als.input.data
@@ -13,12 +13,15 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-# The default LAL script to save all logs, behaving like the versions before 8.5.0.
-rules:
- - name: default
- layer: GENERAL
- dsl: |
- filter {
- sink {
- }
- }
+# Mock input data for envoy-als.yaml rules.
+# The envoy-als rule processes protobuf HTTPAccessLogEntry as extraLog,
+# not JSON body. The proto-json is parsed via protobuf JsonFormat.
+
+envoy-als:
+ body-type: none
+ service: test-mesh-svc
+ extra-log:
+ proto-class: io.envoyproxy.envoy.data.accesslog.v3.HTTPAccessLogEntry
+ proto-json: '{"response":{"responseCode":500},"commonProperties":{"upstreamCluster":"outbound|80||backend.default.svc"}}'
+ expect:
+ tag.status.code: "500"
diff --git a/test/script-cases/scripts/lal/test-lal/envoy-als.yaml b/test/script-cases/scripts/lal/test-lal/oap-cases/envoy-als.yaml
similarity index 100%
rename from test/script-cases/scripts/lal/test-lal/envoy-als.yaml
rename to test/script-cases/scripts/lal/test-lal/oap-cases/envoy-als.yaml
diff --git a/test/script-cases/scripts/lal/test-lal/oap-cases/k8s-service.input.data b/test/script-cases/scripts/lal/test-lal/oap-cases/k8s-service.input.data
new file mode 100644
index 0000000000..11672b05f5
--- /dev/null
+++ b/test/script-cases/scripts/lal/test-lal/oap-cases/k8s-service.input.data
@@ -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.
+
+# Mock input data for k8s-service.yaml rules.
+# Tests the matching path: tag("LOG_KIND") == "NET_PROFILING_SAMPLED_TRACE",
+# which enters the sampledTrace block and populates the SampledTraceBuilder.
+# Uses HTTPS component (parsed.component == "http" && parsed.ssl == true → componentId 129).
+
+network-profiling-slow-trace:
+ body-type: json
+ body: '{"latency":350,"uri":"/k8s/endpoint","reason":"status_5xx","client_process":{"process_id":"k8s-client-proc","local":true,"address":"10.1.0.1:80"},"server_process":{"process_id":"k8s-server-proc","local":false,"address":"10.1.0.2:443"},"detect_point":"server","component":"http","ssl":true}'
+ service: k8s-test-svc
+ instance: k8s-test-instance
+ trace-id: trace-k8s-002
+ timestamp: 1609459300000
+ tags:
+ LOG_KIND: NET_PROFILING_SAMPLED_TRACE
+ expect:
+ save: true
+ abort: false
+ sampledTrace.latency: 350
+ sampledTrace.uri: /k8s/endpoint
+ sampledTrace.reason: STATUS_5XX
+ sampledTrace.processId: k8s-client-proc
+ sampledTrace.destProcessId: k8s-server-proc
+ sampledTrace.detectPoint: SERVER
+ sampledTrace.componentId: 129
diff --git a/test/script-cases/scripts/lal/test-lal/k8s-service.yaml b/test/script-cases/scripts/lal/test-lal/oap-cases/k8s-service.yaml
similarity index 100%
rename from test/script-cases/scripts/lal/test-lal/k8s-service.yaml
rename to test/script-cases/scripts/lal/test-lal/oap-cases/k8s-service.yaml
diff --git a/test/script-cases/scripts/lal/test-lal/oap-cases/mesh-dp.input.data b/test/script-cases/scripts/lal/test-lal/oap-cases/mesh-dp.input.data
new file mode 100644
index 0000000000..b60831681a
--- /dev/null
+++ b/test/script-cases/scripts/lal/test-lal/oap-cases/mesh-dp.input.data
@@ -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.
+
+# Mock input data for mesh-dp.yaml rules.
+# Tests the matching path: tag("LOG_KIND") == "NET_PROFILING_SAMPLED_TRACE",
+# which enters the sampledTrace block and populates the SampledTraceBuilder.
+# We provide process_id directly to avoid K8s/ProcessRegistry dependency.
+
+network-profiling-slow-trace:
+ body-type: json
+ body: '{"latency":200,"uri":"/mesh/api","reason":"slow","client_process":{"process_id":"client-proc-1","local":false,"address":"10.0.0.1:8080"},"server_process":{"process_id":"server-proc-2","local":true,"address":"10.0.0.2:9090"},"detect_point":"client","component":"http","ssl":false}'
+ service: mesh-test-svc
+ instance: mesh-test-instance
+ trace-id: trace-mesh-001
+ timestamp: 1609459200000
+ tags:
+ LOG_KIND: NET_PROFILING_SAMPLED_TRACE
+ expect:
+ save: true
+ abort: false
+ sampledTrace.latency: 200
+ sampledTrace.uri: /mesh/api
+ sampledTrace.reason: SLOW
+ sampledTrace.processId: client-proc-1
+ sampledTrace.destProcessId: server-proc-2
+ sampledTrace.detectPoint: CLIENT
+ sampledTrace.componentId: 49
diff --git a/test/script-cases/scripts/lal/test-lal/mesh-dp.yaml b/test/script-cases/scripts/lal/test-lal/oap-cases/mesh-dp.yaml
similarity index 100%
rename from test/script-cases/scripts/lal/test-lal/mesh-dp.yaml
rename to test/script-cases/scripts/lal/test-lal/oap-cases/mesh-dp.yaml
diff --git a/test/script-cases/scripts/lal/test-lal/default.yaml b/test/script-cases/scripts/lal/test-lal/oap-cases/mysql-slowsql.input.data
similarity index 71%
copy from test/script-cases/scripts/lal/test-lal/default.yaml
copy to test/script-cases/scripts/lal/test-lal/oap-cases/mysql-slowsql.input.data
index 12317a95bf..49d3f2ec27 100644
--- a/test/script-cases/scripts/lal/test-lal/default.yaml
+++ b/test/script-cases/scripts/lal/test-lal/oap-cases/mysql-slowsql.input.data
@@ -13,12 +13,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-# The default LAL script to save all logs, behaving like the versions before 8.5.0.
-rules:
- - name: default
- layer: GENERAL
- dsl: |
- filter {
- sink {
- }
- }
+# Mock input data for mysql-slowsql.yaml rules.
+
+mysql-slowsql:
+ body-type: json
+ body: '{"layer":"MYSQL","service":"db-svc","time":"1609459200000","id":"slow-1","statement":"SELECT 1","query_time":500}'
+ tags:
+ LOG_KIND: SLOW_SQL
+ expect:
+ service: db-svc
+ layer: MYSQL
+ timestamp: 1609459200000
diff --git a/test/script-cases/scripts/lal/test-lal/mysql-slowsql.yaml b/test/script-cases/scripts/lal/test-lal/oap-cases/mysql-slowsql.yaml
similarity index 100%
rename from test/script-cases/scripts/lal/test-lal/mysql-slowsql.yaml
rename to test/script-cases/scripts/lal/test-lal/oap-cases/mysql-slowsql.yaml
diff --git a/test/script-cases/scripts/lal/test-lal/default.yaml b/test/script-cases/scripts/lal/test-lal/oap-cases/nginx.input.data
similarity index 76%
copy from test/script-cases/scripts/lal/test-lal/default.yaml
copy to test/script-cases/scripts/lal/test-lal/oap-cases/nginx.input.data
index 12317a95bf..21ea01f7c1 100644
--- a/test/script-cases/scripts/lal/test-lal/default.yaml
+++ b/test/script-cases/scripts/lal/test-lal/oap-cases/nginx.input.data
@@ -13,12 +13,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-# The default LAL script to save all logs, behaving like the versions before 8.5.0.
-rules:
- - name: default
- layer: GENERAL
- dsl: |
- filter {
- sink {
- }
- }
+# Mock input data for nginx.yaml rules.
+
+nginx-access-log:
+ body-type: text
+ body: '10.0.0.1 - - [01/Jan/2021:00:00:00 +0000] "GET /api HTTP/1.1" 200 1234'
+ tags:
+ LOG_KIND: NGINX_ACCESS_LOG
+ expect:
+ tag.http.status_code: "200"
diff --git a/test/script-cases/scripts/lal/test-lal/nginx.yaml b/test/script-cases/scripts/lal/test-lal/oap-cases/nginx.yaml
similarity index 100%
rename from test/script-cases/scripts/lal/test-lal/nginx.yaml
rename to test/script-cases/scripts/lal/test-lal/oap-cases/nginx.yaml
diff --git a/test/script-cases/scripts/lal/test-lal/default.yaml b/test/script-cases/scripts/lal/test-lal/oap-cases/pgsql-slowsql.input.data
similarity index 70%
copy from test/script-cases/scripts/lal/test-lal/default.yaml
copy to test/script-cases/scripts/lal/test-lal/oap-cases/pgsql-slowsql.input.data
index 12317a95bf..ffdae9b64b 100644
--- a/test/script-cases/scripts/lal/test-lal/default.yaml
+++ b/test/script-cases/scripts/lal/test-lal/oap-cases/pgsql-slowsql.input.data
@@ -13,12 +13,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-# The default LAL script to save all logs, behaving like the versions before 8.5.0.
-rules:
- - name: default
- layer: GENERAL
- dsl: |
- filter {
- sink {
- }
- }
+# Mock input data for pgsql-slowsql.yaml rules.
+
+pgsql-slowsql:
+ body-type: json
+ body: '{"layer":"POSTGRESQL","service":"pg-svc","time":"1609459200000","id":"slow-pg-1","statement":"SELECT 1","query_time":300}'
+ tags:
+ LOG_KIND: SLOW_SQL
+ expect:
+ service: pg-svc
+ layer: POSTGRESQL
+ timestamp: 1609459200000
diff --git a/test/script-cases/scripts/lal/test-lal/pgsql-slowsql.yaml b/test/script-cases/scripts/lal/test-lal/oap-cases/pgsql-slowsql.yaml
similarity index 100%
rename from test/script-cases/scripts/lal/test-lal/pgsql-slowsql.yaml
rename to test/script-cases/scripts/lal/test-lal/oap-cases/pgsql-slowsql.yaml
diff --git a/test/script-cases/scripts/lal/test-lal/default.yaml b/test/script-cases/scripts/lal/test-lal/oap-cases/redis-slowsql.input.data
similarity index 70%
rename from test/script-cases/scripts/lal/test-lal/default.yaml
rename to test/script-cases/scripts/lal/test-lal/oap-cases/redis-slowsql.input.data
index 12317a95bf..95dc05b494 100644
--- a/test/script-cases/scripts/lal/test-lal/default.yaml
+++ b/test/script-cases/scripts/lal/test-lal/oap-cases/redis-slowsql.input.data
@@ -13,12 +13,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-# The default LAL script to save all logs, behaving like the versions before 8.5.0.
-rules:
- - name: default
- layer: GENERAL
- dsl: |
- filter {
- sink {
- }
- }
+# Mock input data for redis-slowsql.yaml rules.
+
+redis-slowsql:
+ body-type: json
+ body: '{"layer":"REDIS","service":"redis-svc","time":"1609459200000","id":"slow-redis-1","statement":"GET key","query_time":200}'
+ tags:
+ LOG_KIND: SLOW_SQL
+ expect:
+ service: redis-svc
+ layer: REDIS
+ timestamp: 1609459200000
diff --git a/test/script-cases/scripts/lal/test-lal/redis-slowsql.yaml b/test/script-cases/scripts/lal/test-lal/oap-cases/redis-slowsql.yaml
similarity index 100%
rename from test/script-cases/scripts/lal/test-lal/redis-slowsql.yaml
rename to test/script-cases/scripts/lal/test-lal/oap-cases/redis-slowsql.yaml