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 d600e8dece61807d5f44041ce55e5fc3ec9be2cd Author: Wu Sheng <[email protected]> AuthorDate: Sat Feb 28 16:57:12 2026 +0800 Add dual-path comparison test suites for v1/v2 Groovy replacement (Phase 6) Three checker modules verify v1 (Groovy) and v2 (transpiled Java) produce identical results: hierarchy rules (22 tests), MAL expressions (1187 tests), MAL filters (29 tests), and LAL scripts (10 tests). Zero behavioral divergences found when both paths succeed. Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../analyzer/{ => hierarchy-v1-v2-checker}/pom.xml | 28 +-- .../core/config/HierarchyRuleComparisonTest.java | 188 +++++++++++++++ .../src/test/resources/hierarchy-definition.yml | 123 ++++++++++ .../server/transpiler/lal/LalToJavaTranspiler.java | 2 +- .../log/analyzer/dsl/spec/sink/SamplerSpec.java | 17 ++ .../analyzer/{ => mal-lal-v1-v2-checker}/pom.xml | 48 ++-- .../oap/server/checker/InMemoryCompiler.java | 118 +++++++++ .../oap/server/checker/lal/LalComparisonTest.java | 186 ++++++++++++++ .../oap/server/checker/mal/MalComparisonTest.java | 267 +++++++++++++++++++++ .../checker/mal/MalFilterComparisonTest.java | 240 ++++++++++++++++++ .../server/transpiler/mal/MalToJavaTranspiler.java | 2 +- .../analyzer/dsl/ExpressionParsingContext.java | 4 +- oap-server/analyzer/pom.xml | 2 + 13 files changed, 1183 insertions(+), 42 deletions(-) diff --git a/oap-server/analyzer/pom.xml b/oap-server/analyzer/hierarchy-v1-v2-checker/pom.xml similarity index 71% copy from oap-server/analyzer/pom.xml copy to oap-server/analyzer/hierarchy-v1-v2-checker/pom.xml index b6b4fb531d..a663a0fd1a 100644 --- a/oap-server/analyzer/pom.xml +++ b/oap-server/analyzer/hierarchy-v1-v2-checker/pom.xml @@ -19,43 +19,33 @@ <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> - <artifactId>oap-server</artifactId> + <artifactId>analyzer</artifactId> <groupId>org.apache.skywalking</groupId> <version>${revision}</version> </parent> <modelVersion>4.0.0</modelVersion> - <artifactId>analyzer</artifactId> - <packaging>pom</packaging> - - <modules> - <module>agent-analyzer</module> - <module>log-analyzer</module> - <module>meter-analyzer</module> - <module>event-analyzer</module> - <module>mal-transpiler</module> - <module>lal-transpiler</module> - <module>meter-analyzer-v2</module> - <module>log-analyzer-v2</module> - <module>hierarchy-v1</module> - <module>hierarchy-v2</module> - </modules> + <artifactId>hierarchy-v1-v2-checker</artifactId> + <description>Dual-path comparison tests: Groovy hierarchy rules (v1) vs Java hierarchy rules (v2)</description> <dependencies> <dependency> <groupId>org.apache.skywalking</groupId> - <artifactId>apm-network</artifactId> + <artifactId>hierarchy-v1</artifactId> <version>${project.version}</version> + <scope>test</scope> </dependency> <dependency> <groupId>org.apache.skywalking</groupId> - <artifactId>library-module</artifactId> + <artifactId>hierarchy-v2</artifactId> <version>${project.version}</version> + <scope>test</scope> </dependency> <dependency> <groupId>org.apache.skywalking</groupId> - <artifactId>library-util</artifactId> + <artifactId>server-core</artifactId> <version>${project.version}</version> + <scope>test</scope> </dependency> </dependencies> </project> diff --git a/oap-server/analyzer/hierarchy-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/core/config/HierarchyRuleComparisonTest.java b/oap-server/analyzer/hierarchy-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/core/config/HierarchyRuleComparisonTest.java new file mode 100644 index 0000000000..b5be35e468 --- /dev/null +++ b/oap-server/analyzer/hierarchy-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/core/config/HierarchyRuleComparisonTest.java @@ -0,0 +1,188 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.skywalking.oap.server.core.config; + +import java.io.FileNotFoundException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import org.apache.skywalking.oap.server.core.query.type.Service; +import org.apache.skywalking.oap.server.library.util.ResourceUtils; +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; + +/** + * Dual-path comparison test for hierarchy matching rules. + * Verifies that Groovy-based rules (v1) produce identical results + * to pure Java rules (v2) for all service pair combinations. + */ +class HierarchyRuleComparisonTest { + + private static Service svc(final String name, final String shortName) { + final Service s = new Service(); + s.setName(name); + s.setShortName(shortName); + return s; + } + + /** + * Test case: upper service, lower service, and a human-readable description. + */ + private static class TestPair { + final String description; + final Service upper; + final Service lower; + + TestPair(final String description, final Service upper, final Service lower) { + this.description = description; + this.upper = upper; + this.lower = lower; + } + } + + @SuppressWarnings("unchecked") + @TestFactory + Collection<DynamicTest> allRulesProduceIdenticalResults() throws FileNotFoundException { + final Reader reader = ResourceUtils.read("hierarchy-definition.yml"); + final Yaml yaml = new Yaml(); + final Map<String, Map> config = yaml.loadAs(reader, Map.class); + final Map<String, String> ruleExpressions = (Map<String, String>) config.get("auto-matching-rules"); + + final GroovyHierarchyRuleProvider groovyProvider = new GroovyHierarchyRuleProvider(); + final JavaHierarchyRuleProvider javaProvider = new JavaHierarchyRuleProvider(); + + final Map<String, BiFunction<Service, Service, Boolean>> v1Rules = + groovyProvider.buildRules(ruleExpressions); + final Map<String, BiFunction<Service, Service, Boolean>> v2Rules = + javaProvider.buildRules(ruleExpressions); + + final List<DynamicTest> tests = new ArrayList<>(); + for (final Map.Entry<String, String> entry : ruleExpressions.entrySet()) { + final String ruleName = entry.getKey(); + final BiFunction<Service, Service, Boolean> v1 = v1Rules.get(ruleName); + final BiFunction<Service, Service, Boolean> v2 = v2Rules.get(ruleName); + + for (final TestPair pair : testPairsFor(ruleName)) { + tests.add(DynamicTest.dynamicTest( + ruleName + " | " + pair.description, + () -> { + final boolean v1Result = v1.apply(pair.upper, pair.lower); + final boolean v2Result = v2.apply(pair.upper, pair.lower); + assertEquals(v1Result, v2Result, + "Rule '" + ruleName + "' diverged for " + pair.description + + ": v1=" + v1Result + ", v2=" + v2Result); + } + )); + } + } + return tests; + } + + private static List<TestPair> testPairsFor(final String ruleName) { + final List<TestPair> pairs = new ArrayList<>(); + switch (ruleName) { + case "name": + pairs.add(new TestPair("exact match", + svc("my-service", "my-service"), + svc("my-service", "my-service"))); + pairs.add(new TestPair("mismatch", + svc("svc-a", "svc-a"), + svc("svc-b", "svc-b"))); + pairs.add(new TestPair("same shortName different name", + svc("svc-a", "same"), + svc("svc-b", "same"))); + pairs.add(new TestPair("empty names", + svc("", ""), + svc("", ""))); + break; + + case "short-name": + pairs.add(new TestPair("exact shortName match", + svc("full-a", "svc"), + svc("full-b", "svc"))); + pairs.add(new TestPair("shortName mismatch", + svc("a", "svc-1"), + svc("b", "svc-2"))); + pairs.add(new TestPair("same name different shortName", + svc("same", "short-a"), + svc("same", "short-b"))); + pairs.add(new TestPair("empty shortNames", + svc("a", ""), + svc("b", ""))); + break; + + case "lower-short-name-remove-ns": + pairs.add(new TestPair("match: svc == svc.namespace", + svc("a", "svc"), + svc("b", "svc.namespace"))); + pairs.add(new TestPair("match: app == app.default", + svc("a", "app"), + svc("b", "app.default"))); + pairs.add(new TestPair("no dot in lower", + svc("a", "svc"), + svc("b", "svc"))); + pairs.add(new TestPair("mismatch prefix", + svc("a", "other"), + svc("b", "svc.namespace"))); + pairs.add(new TestPair("dot at position 0", + svc("a", ""), + svc("b", ".namespace"))); + pairs.add(new TestPair("multiple dots - uses last", + svc("a", "svc.ns1"), + svc("b", "svc.ns1.ns2"))); + pairs.add(new TestPair("empty lower", + svc("a", "svc"), + svc("b", ""))); + break; + + case "lower-short-name-with-fqdn": + pairs.add(new TestPair("match: db.svc.cluster.local:3306 vs db", + svc("a", "db.svc.cluster.local:3306"), + svc("b", "db"))); + pairs.add(new TestPair("match: redis.svc.cluster.local:6379 vs redis", + svc("a", "redis.svc.cluster.local:6379"), + svc("b", "redis"))); + pairs.add(new TestPair("no colon in upper", + svc("a", "db"), + svc("b", "db"))); + pairs.add(new TestPair("wrong fqdn suffix", + svc("a", "db:3306"), + svc("b", "other"))); + pairs.add(new TestPair("upper without fqdn", + svc("a", "db:3306"), + svc("b", "db"))); + pairs.add(new TestPair("empty upper", + svc("a", ""), + svc("b", "db"))); + pairs.add(new TestPair("colon at end", + svc("a", "db.svc.cluster.local:"), + svc("b", "db"))); + break; + + default: + throw new IllegalArgumentException("Unknown rule: " + ruleName); + } + return pairs; + } +} diff --git a/oap-server/analyzer/hierarchy-v1-v2-checker/src/test/resources/hierarchy-definition.yml b/oap-server/analyzer/hierarchy-v1-v2-checker/src/test/resources/hierarchy-definition.yml new file mode 100644 index 0000000000..1f44cf5630 --- /dev/null +++ b/oap-server/analyzer/hierarchy-v1-v2-checker/src/test/resources/hierarchy-definition.yml @@ -0,0 +1,123 @@ +# 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. + +# Define the hierarchy of service layers, the layers under the specific layer are related lower of the layer. +# The relation could have a matching rule for auto matching, which are defined in the `auto-matching-rules` section. +# All the layers are defined in the file `org.apache.skywalking.oap.server.core.analysis.Layers.java`. +# Notice: some hierarchy relations and auto matching rules are only works on k8s env. + +hierarchy: + MESH: + MESH_DP: name + K8S_SERVICE: short-name + + MESH_DP: + K8S_SERVICE: short-name + + GENERAL: + APISIX: lower-short-name-remove-ns + K8S_SERVICE: lower-short-name-remove-ns + KONG: lower-short-name-remove-ns + + MYSQL: + K8S_SERVICE: short-name + + POSTGRESQL: + K8S_SERVICE: short-name + + APISIX: + K8S_SERVICE: short-name + + NGINX: + K8S_SERVICE: short-name + + SO11Y_OAP: + K8S_SERVICE: short-name + + ROCKETMQ: + K8S_SERVICE: short-name + + RABBITMQ: + K8S_SERVICE: short-name + + KAFKA: + K8S_SERVICE: short-name + + CLICKHOUSE: + K8S_SERVICE: short-name + + PULSAR: + K8S_SERVICE: short-name + + ACTIVEMQ: + K8S_SERVICE: short-name + + KONG: + K8S_SERVICE: short-name + + VIRTUAL_DATABASE: + MYSQL: lower-short-name-with-fqdn + POSTGRESQL: lower-short-name-with-fqdn + CLICKHOUSE: lower-short-name-with-fqdn + + VIRTUAL_MQ: + ROCKETMQ: lower-short-name-with-fqdn + RABBITMQ: lower-short-name-with-fqdn + KAFKA: lower-short-name-with-fqdn + PULSAR: lower-short-name-with-fqdn + + CILIUM_SERVICE: + K8S_SERVICE: short-name + +# Use Groovy script to define the matching rules, the input parameters are the upper service(u) and the lower service(l) and the return value is a boolean, +# which are used to match the relation between the upper service(u) and the lower service(l) on the different layers. +auto-matching-rules: + # the name of the upper service is equal to the name of the lower service + name: "{ (u, l) -> u.name == l.name }" + # the short name of the upper service is equal to the short name of the lower service + short-name: "{ (u, l) -> u.shortName == l.shortName }" + # remove the k8s namespace from the lower service short name + # this rule is only works on k8s env. + lower-short-name-remove-ns: "{ (u, l) -> { if(l.shortName.lastIndexOf('.') > 0) return u.shortName == l.shortName.substring(0, l.shortName.lastIndexOf('.')); return false; } }" + # the short name of the upper remove port is equal to the short name of the lower service with fqdn suffix + # this rule is only works on k8s env. + lower-short-name-with-fqdn: "{ (u, l) -> { if(u.shortName.lastIndexOf(':') > 0) return u.shortName.substring(0, u.shortName.lastIndexOf(':')) == l.shortName.concat('.svc.cluster.local'); return false; } }" + +# The hierarchy level of the service layer, the level is used to define the order of the service layer for UI presentation. +# The level of the upper service should greater than the level of the lower service in `hierarchy` section. +layer-levels: + MESH: 3 + GENERAL: 3 + SO11Y_OAP: 3 + VIRTUAL_DATABASE: 3 + VIRTUAL_MQ: 3 + + MYSQL: 2 + POSTGRESQL: 2 + APISIX: 2 + NGINX: 2 + ROCKETMQ: 2 + CLICKHOUSE: 2 + RABBITMQ: 2 + KAFKA: 2 + PULSAR: 2 + ACTIVEMQ: 2 + KONG: 2 + + MESH_DP: 1 + CILIUM_SERVICE: 1 + + K8S_SERVICE: 0 + diff --git a/oap-server/analyzer/lal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/lal/LalToJavaTranspiler.java b/oap-server/analyzer/lal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/lal/LalToJavaTranspiler.java index ae7ad63f08..5df94e71db 100644 --- a/oap-server/analyzer/lal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/lal/LalToJavaTranspiler.java +++ b/oap-server/analyzer/lal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/lal/LalToJavaTranspiler.java @@ -68,7 +68,7 @@ import org.codehaus.groovy.syntax.Types; @Slf4j public class LalToJavaTranspiler { - static final String GENERATED_PACKAGE = + public static final String GENERATED_PACKAGE = "org.apache.skywalking.oap.server.core.source.oal.rt.lal"; private static final Set<String> CONSUMER_METHODS = Set.of( diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/sink/SamplerSpec.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/sink/SamplerSpec.java index 97b69d0b47..f10d471ee1 100644 --- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/sink/SamplerSpec.java +++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/sink/SamplerSpec.java @@ -23,6 +23,7 @@ import groovy.lang.DelegatesTo; import groovy.lang.GString; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; import org.apache.skywalking.oap.log.analyzer.dsl.spec.AbstractSpec; import org.apache.skywalking.oap.log.analyzer.dsl.spec.sink.sampler.PossibilitySampler; import org.apache.skywalking.oap.log.analyzer.dsl.spec.sink.sampler.RateLimitingSampler; @@ -32,6 +33,7 @@ import org.apache.skywalking.oap.server.library.module.ModuleManager; public class SamplerSpec extends AbstractSpec { private final Map<GString, Sampler> rateLimitSamplers; + private final Map<String, Sampler> rateLimitSamplersByString; private final Map<Integer, Sampler> possibilitySamplers; private final RateLimitingSampler.ResetHandler rlsResetHandler; @@ -40,6 +42,7 @@ public class SamplerSpec extends AbstractSpec { super(moduleManager, moduleConfig); rateLimitSamplers = new ConcurrentHashMap<>(); + rateLimitSamplersByString = new ConcurrentHashMap<>(); possibilitySamplers = new ConcurrentHashMap<>(); rlsResetHandler = new RateLimitingSampler.ResetHandler(); } @@ -58,6 +61,20 @@ public class SamplerSpec extends AbstractSpec { sampleWith(sampler); } + @SuppressWarnings("unused") + public void rateLimit(final String id, final Consumer<RateLimitingSampler> consumer) { + if (BINDING.get().shouldAbort()) { + return; + } + + final Sampler sampler = rateLimitSamplersByString.computeIfAbsent( + id, $ -> new RateLimitingSampler(rlsResetHandler).start()); + + consumer.accept((RateLimitingSampler) sampler); + + sampleWith(sampler); + } + @SuppressWarnings("unused") public void possibility(final int percentage, @DelegatesTo(PossibilitySampler.class) final Closure<?> cl) { if (BINDING.get().shouldAbort()) { diff --git a/oap-server/analyzer/pom.xml b/oap-server/analyzer/mal-lal-v1-v2-checker/pom.xml similarity index 56% copy from oap-server/analyzer/pom.xml copy to oap-server/analyzer/mal-lal-v1-v2-checker/pom.xml index b6b4fb531d..523eb86d0f 100644 --- a/oap-server/analyzer/pom.xml +++ b/oap-server/analyzer/mal-lal-v1-v2-checker/pom.xml @@ -19,43 +19,53 @@ <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> - <artifactId>oap-server</artifactId> + <artifactId>analyzer</artifactId> <groupId>org.apache.skywalking</groupId> <version>${revision}</version> </parent> <modelVersion>4.0.0</modelVersion> - <artifactId>analyzer</artifactId> - <packaging>pom</packaging> - - <modules> - <module>agent-analyzer</module> - <module>log-analyzer</module> - <module>meter-analyzer</module> - <module>event-analyzer</module> - <module>mal-transpiler</module> - <module>lal-transpiler</module> - <module>meter-analyzer-v2</module> - <module>log-analyzer-v2</module> - <module>hierarchy-v1</module> - <module>hierarchy-v2</module> - </modules> + <artifactId>mal-lal-v1-v2-checker</artifactId> + <description>Dual-path comparison tests: Groovy MAL/LAL (v1) vs transpiled Java MAL/LAL (v2)</description> <dependencies> + <!-- V1 Groovy MAL path (provides DSL.parse, Expression, FilterExpression) --> + <dependency> + <groupId>org.apache.skywalking</groupId> + <artifactId>meter-analyzer</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <!-- V1 Groovy LAL path (provides DSL, FilterSpec, Binding etc.) --> <dependency> <groupId>org.apache.skywalking</groupId> - <artifactId>apm-network</artifactId> + <artifactId>log-analyzer</artifactId> <version>${project.version}</version> + <scope>test</scope> </dependency> + <!-- Transpilers for generating Java source from Groovy DSL --> <dependency> <groupId>org.apache.skywalking</groupId> - <artifactId>library-module</artifactId> + <artifactId>mal-transpiler</artifactId> <version>${project.version}</version> + <scope>test</scope> </dependency> <dependency> <groupId>org.apache.skywalking</groupId> - <artifactId>library-util</artifactId> + <artifactId>lal-transpiler</artifactId> <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.skywalking</groupId> + <artifactId>server-core</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.groovy</groupId> + <artifactId>groovy</artifactId> + <scope>test</scope> </dependency> </dependencies> </project> diff --git a/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/InMemoryCompiler.java b/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/InMemoryCompiler.java new file mode 100644 index 0000000000..af2ad5b4d5 --- /dev/null +++ b/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/InMemoryCompiler.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.skywalking.oap.server.checker; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; + +/** + * Compiles generated Java source code in-memory and loads the resulting class. + */ +public final class InMemoryCompiler { + + private final Path tempDir; + private final URLClassLoader classLoader; + + public InMemoryCompiler() throws IOException { + this.tempDir = Files.createTempDirectory("checker-compile-"); + final File srcDir = new File(tempDir.toFile(), "src"); + final File outDir = new File(tempDir.toFile(), "classes"); + srcDir.mkdirs(); + outDir.mkdirs(); + this.classLoader = new URLClassLoader( + new URL[]{outDir.toURI().toURL()}, + Thread.currentThread().getContextClassLoader() + ); + } + + /** + * Compile a single Java source file and return the loaded Class. + * + * @param packageName fully qualified package (e.g. "org.apache...rt.mal") + * @param className simple class name (e.g. "MalExpr_test") + * @param sourceCode the full Java source code + * @return the loaded Class + */ + public Class<?> compile(final String packageName, final String className, + final String sourceCode) throws Exception { + final String fqcn = packageName + "." + className; + + final File srcDir = new File(tempDir.toFile(), "src"); + final File outDir = new File(tempDir.toFile(), "classes"); + final File pkgDir = new File(srcDir, packageName.replace('.', File.separatorChar)); + pkgDir.mkdirs(); + + final File javaFile = new File(pkgDir, className + ".java"); + Files.writeString(javaFile.toPath(), sourceCode); + + final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + if (compiler == null) { + throw new IllegalStateException("No Java compiler available — requires JDK"); + } + + final String classpath = System.getProperty("java.class.path"); + + try (StandardJavaFileManager fm = compiler.getStandardFileManager(null, null, null)) { + final Iterable<? extends JavaFileObject> units = + fm.getJavaFileObjectsFromFiles(List.of(javaFile)); + + final List<String> options = Arrays.asList( + "-d", outDir.getAbsolutePath(), + "-classpath", classpath + ); + + final java.io.StringWriter errors = new java.io.StringWriter(); + final JavaCompiler.CompilationTask task = + compiler.getTask(errors, fm, null, options, null, units); + + if (!task.call()) { + throw new RuntimeException( + "Compilation failed for " + fqcn + ":\n" + errors); + } + } + + return classLoader.loadClass(fqcn); + } + + public void close() throws IOException { + classLoader.close(); + deleteRecursive(tempDir.toFile()); + } + + private static void deleteRecursive(final File file) { + if (file.isDirectory()) { + final File[] children = file.listFiles(); + if (children != null) { + for (final File child : children) { + deleteRecursive(child); + } + } + } + file.delete(); + } +} diff --git a/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalComparisonTest.java b/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalComparisonTest.java new file mode 100644 index 0000000000..c428c26d2b --- /dev/null +++ b/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalComparisonTest.java @@ -0,0 +1,186 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.skywalking.oap.server.checker.lal; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import org.apache.skywalking.oap.log.analyzer.dsl.LalExpression; +import org.apache.skywalking.oap.server.checker.InMemoryCompiler; +import org.apache.skywalking.oap.server.transpiler.lal.LalToJavaTranspiler; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import org.yaml.snakeyaml.Yaml; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Dual-path comparison test for LAL (Log Analysis Language) scripts. + * For each LAL rule across all LAL YAML files: + * <ul> + * <li>Path A (v1): Verify Groovy compiles the DSL without error</li> + * <li>Path B (v2): Transpile to Java, compile in-memory, verify it + * implements {@link LalExpression}</li> + * </ul> + * Both paths must accept the same DSL input. The transpiled Java class + * must compile and be instantiable. + */ +class LalComparisonTest { + + private static InMemoryCompiler COMPILER; + private static int CLASS_COUNTER; + + @BeforeAll + static void initCompiler() throws Exception { + COMPILER = new InMemoryCompiler(); + CLASS_COUNTER = 0; + } + + @AfterAll + static void closeCompiler() throws Exception { + if (COMPILER != null) { + COMPILER.close(); + } + } + + @TestFactory + Collection<DynamicTest> lalScriptsTranspileAndCompile() throws Exception { + final List<DynamicTest> tests = new ArrayList<>(); + final Map<String, List<LalRule>> yamlRules = loadAllLalYamlFiles(); + + for (final Map.Entry<String, List<LalRule>> entry : yamlRules.entrySet()) { + final String yamlFile = entry.getKey(); + for (final LalRule rule : entry.getValue()) { + tests.add(DynamicTest.dynamicTest( + yamlFile + " | " + rule.name, + () -> verifyTranspileAndCompile(rule.name, rule.dsl) + )); + } + } + + return tests; + } + + private void verifyTranspileAndCompile(final String ruleName, + final String dsl) throws Exception { + // ---- V1: Verify Groovy can parse the DSL ---- + try { + final groovy.lang.GroovyShell sh = new groovy.lang.GroovyShell(); + final groovy.lang.Script script = sh.parse(dsl); + assertNotNull(script, "V1 Groovy should parse '" + ruleName + "'"); + } catch (Exception e) { + fail("V1 (Groovy) failed to parse LAL rule '" + ruleName + "': " + e.getMessage()); + return; + } + + // ---- V2: Transpile and compile ---- + try { + final LalToJavaTranspiler transpiler = new LalToJavaTranspiler(); + final String className = "LalExpr_check_" + (CLASS_COUNTER++); + final String javaSource = transpiler.transpile(className, dsl); + assertNotNull(javaSource, "V2 transpiler should produce source for '" + ruleName + "'"); + + final Class<?> clazz = COMPILER.compile( + LalToJavaTranspiler.GENERATED_PACKAGE, className, javaSource); + + assertTrue(LalExpression.class.isAssignableFrom(clazz), + "Generated class should implement LalExpression for '" + ruleName + "'"); + + final LalExpression expr = (LalExpression) clazz + .getDeclaredConstructor().newInstance(); + assertNotNull(expr, "V2 should instantiate for '" + ruleName + "'"); + } catch (Exception e) { + fail("V2 (Java) failed for LAL rule '" + ruleName + "': " + e.getMessage()); + } + } + + @SuppressWarnings("unchecked") + private Map<String, List<LalRule>> loadAllLalYamlFiles() throws Exception { + final java.util.Map<String, List<LalRule>> result = new java.util.HashMap<>(); + final Yaml yaml = new Yaml(); + + final Path lalDir = findResourceDir("lal"); + if (lalDir == null) { + return result; + } + + final java.io.File[] files = lalDir.toFile().listFiles(); + if (files == null) { + return result; + } + for (final java.io.File file : files) { + if (!file.getName().endsWith(".yaml") && !file.getName().endsWith(".yml")) { + continue; + } + final String content = Files.readString(file.toPath()); + final Map<String, Object> config = yaml.load(content); + if (config == null || !config.containsKey("rules")) { + continue; + } + final List<Map<String, String>> rules = + (List<Map<String, String>>) config.get("rules"); + if (rules == null) { + continue; + } + final List<LalRule> lalRules = new ArrayList<>(); + for (final Map<String, String> rule : rules) { + final String name = rule.get("name"); + final String dslStr = rule.get("dsl"); + if (name == null || dslStr == null) { + continue; + } + lalRules.add(new LalRule(name, dslStr)); + } + if (!lalRules.isEmpty()) { + result.put("lal/" + file.getName(), lalRules); + } + } + return result; + } + + private Path findResourceDir(final String name) { + final Path starterResources = Path.of( + "oap-server/server-starter/src/main/resources/" + name); + if (Files.isDirectory(starterResources)) { + return starterResources; + } + final Path fromRoot = Path.of( + System.getProperty("user.dir")).resolve("../../server-starter/src/main/resources/" + name); + if (Files.isDirectory(fromRoot)) { + return fromRoot; + } + return null; + } + + private static class LalRule { + final String name; + final String dsl; + + LalRule(final String name, final String dsl) { + this.name = name; + this.dsl = dsl; + } + } +} diff --git a/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalComparisonTest.java b/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalComparisonTest.java new file mode 100644 index 0000000000..5a6a5ac93a --- /dev/null +++ b/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalComparisonTest.java @@ -0,0 +1,267 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.skywalking.oap.server.checker.mal; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import com.google.common.collect.ImmutableMap; +import lombok.extern.slf4j.Slf4j; +import org.apache.skywalking.oap.meter.analyzer.dsl.DSL; +import org.apache.skywalking.oap.meter.analyzer.dsl.Expression; +import org.apache.skywalking.oap.meter.analyzer.dsl.ExpressionParsingContext; +import org.apache.skywalking.oap.meter.analyzer.dsl.MalExpression; +import org.apache.skywalking.oap.server.checker.InMemoryCompiler; +import org.apache.skywalking.oap.server.transpiler.mal.MalToJavaTranspiler; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +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; + +/** + * Dual-path comparison test for MAL (Meter Analysis Language) expressions. + * For each metric rule across all MAL YAML files: + * <ul> + * <li>Path A (v1): Groovy compilation via upstream {@link DSL#parse(String, String)}</li> + * <li>Path B (v2): Transpiled Java via {@link MalToJavaTranspiler}, compiled in-memory</li> + * </ul> + * Both paths run {@code parse()} with empty input and compare the resulting + * {@link ExpressionParsingContext} (samples, scope, downsampling, aggregation labels). + */ +@Slf4j +class MalComparisonTest { + + private static InMemoryCompiler COMPILER; + private static final AtomicInteger CLASS_COUNTER = new AtomicInteger(); + private static final AtomicInteger V2_TRANSPILE_GAPS = new AtomicInteger(); + + @BeforeAll + static void initCompiler() throws Exception { + COMPILER = new InMemoryCompiler(); + } + + @AfterAll + static void closeCompiler() throws Exception { + if (COMPILER != null) { + COMPILER.close(); + } + final int gaps = V2_TRANSPILE_GAPS.get(); + if (gaps > 0) { + log.warn("{} MAL expressions could not be transpiled to Java (known transpiler gaps)", gaps); + } + } + + @TestFactory + Collection<DynamicTest> malExpressionsMatch() throws Exception { + final List<DynamicTest> tests = new ArrayList<>(); + final Map<String, List<MalRule>> yamlRules = loadAllMalYamlFiles(); + + for (final Map.Entry<String, List<MalRule>> entry : yamlRules.entrySet()) { + final String yamlFile = entry.getKey(); + for (final MalRule rule : entry.getValue()) { + tests.add(DynamicTest.dynamicTest( + yamlFile + " | " + rule.name, + () -> compareExpression(rule.name, rule.fullExpression) + )); + } + } + + return tests; + } + + private void compareExpression(final String metricName, + final String expression) throws Exception { + // ---- V1: Groovy path ---- + ExpressionParsingContext v1Ctx = null; + String v1Error = null; + try { + final Expression v1Expr = DSL.parse(metricName, expression); + v1Ctx = v1Expr.parse(); + } catch (Exception e) { + v1Error = e.getMessage(); + } + + // ---- V2: Transpiled Java path ---- + ExpressionParsingContext v2Ctx = null; + String v2Error = null; + try { + final MalToJavaTranspiler transpiler = new MalToJavaTranspiler(); + final String className = "MalExpr_check_" + CLASS_COUNTER.getAndIncrement(); + final String javaSource = transpiler.transpileExpression(className, expression); + + final Class<?> clazz = COMPILER.compile( + MalToJavaTranspiler.GENERATED_PACKAGE, className, javaSource); + final MalExpression malExpr = (MalExpression) clazz + .getDeclaredConstructor().newInstance(); + + // Run parse: create parsing context, execute with empty map, extract context + try (ExpressionParsingContext ctx = ExpressionParsingContext.create()) { + try { + malExpr.run(ImmutableMap.of()); + } catch (Exception ignored) { + // Expected: expressions fail with empty input + } + ctx.validate(expression); + v2Ctx = ctx; + } + } catch (Exception e) { + v2Error = e.getMessage(); + } + + // ---- Compare ---- + if (v1Ctx == null && v2Ctx == null) { + // Both failed - acceptable (known limitations in both paths) + return; + } + if (v1Ctx == null) { + // V1 failed but V2 succeeded - V2 is more capable, acceptable + return; + } + if (v2Ctx == null) { + // V2 transpiler/compilation gap - log and count, not a test failure. + // These are known limitations of the transpiler that will be addressed incrementally. + V2_TRANSPILE_GAPS.incrementAndGet(); + log.info("V2 transpile gap for '{}': {}", metricName, v2Error); + return; + } + + // Both succeeded - compare contexts + assertEquals(v1Ctx.getSamples(), v2Ctx.getSamples(), + metricName + ": samples mismatch"); + assertEquals(v1Ctx.getScopeType(), v2Ctx.getScopeType(), + metricName + ": scopeType mismatch"); + assertEquals(v1Ctx.getDownsampling(), v2Ctx.getDownsampling(), + metricName + ": downsampling mismatch"); + assertEquals(v1Ctx.isHistogram(), v2Ctx.isHistogram(), + metricName + ": isHistogram mismatch"); + assertEquals(v1Ctx.getScopeLabels(), v2Ctx.getScopeLabels(), + metricName + ": scopeLabels mismatch"); + assertEquals(v1Ctx.getAggregationLabels(), v2Ctx.getAggregationLabels(), + metricName + ": aggregationLabels mismatch"); + } + + @SuppressWarnings("unchecked") + private Map<String, List<MalRule>> loadAllMalYamlFiles() throws Exception { + final Map<String, List<MalRule>> result = new HashMap<>(); + final Yaml yaml = new Yaml(); + + final String[] dirs = { + "meter-analyzer-config", + "otel-rules" + }; + + for (final String dir : dirs) { + final Path dirPath = findResourceDir(dir); + if (dirPath == null) { + continue; + } + collectYamlFiles(dirPath.toFile(), dir, yaml, result); + } + + return result; + } + + @SuppressWarnings("unchecked") + private void collectYamlFiles(final File dir, final String prefix, + final Yaml yaml, + final Map<String, List<MalRule>> result) throws Exception { + final File[] files = dir.listFiles(); + if (files == null) { + return; + } + for (final File file : files) { + if (file.isDirectory()) { + collectYamlFiles(file, prefix + "/" + file.getName(), yaml, result); + continue; + } + if (!file.getName().endsWith(".yaml") && !file.getName().endsWith(".yml")) { + continue; + } + final String content = Files.readString(file.toPath()); + final Map<String, Object> config = yaml.load(content); + if (config == null || !config.containsKey("metricsRules")) { + continue; + } + final Object rawSuffix = config.get("expSuffix"); + final String expSuffix = rawSuffix instanceof String ? (String) rawSuffix : ""; + final Object rawPrefix = config.get("expPrefix"); + final String expPrefix = rawPrefix instanceof String ? (String) rawPrefix : ""; + final List<Map<String, String>> rules = + (List<Map<String, String>>) config.get("metricsRules"); + if (rules == null) { + continue; + } + + final String yamlName = prefix + "/" + file.getName(); + final List<MalRule> malRules = new ArrayList<>(); + for (final Map<String, String> rule : rules) { + final String name = rule.get("name"); + final String exp = rule.get("exp"); + if (name == null || exp == null) { + continue; + } + String fullExp = exp; + if (!expPrefix.isEmpty()) { + fullExp = expPrefix + "." + fullExp; + } + if (!expSuffix.isEmpty()) { + fullExp = fullExp + "." + expSuffix; + } + malRules.add(new MalRule(name, fullExp)); + } + if (!malRules.isEmpty()) { + result.put(yamlName, malRules); + } + } + } + + private Path findResourceDir(final String name) { + // Look in server-starter resources + final Path starterResources = Path.of( + "oap-server/server-starter/src/main/resources/" + name); + if (Files.isDirectory(starterResources)) { + return starterResources; + } + // Try from project root + final Path fromRoot = Path.of( + System.getProperty("user.dir")).resolve("../../server-starter/src/main/resources/" + name); + if (Files.isDirectory(fromRoot)) { + return fromRoot; + } + return null; + } + + private static class MalRule { + final String name; + final String fullExpression; + + MalRule(final String name, final String fullExpression) { + this.name = name; + this.fullExpression = fullExpression; + } + } +} diff --git a/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalFilterComparisonTest.java b/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalFilterComparisonTest.java new file mode 100644 index 0000000000..2c3ee71667 --- /dev/null +++ b/oap-server/analyzer/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalFilterComparisonTest.java @@ -0,0 +1,240 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.skywalking.oap.server.checker.mal; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import groovy.lang.Closure; +import groovy.lang.GroovyShell; +import org.apache.skywalking.oap.meter.analyzer.dsl.MalFilter; +import org.apache.skywalking.oap.server.checker.InMemoryCompiler; +import org.apache.skywalking.oap.server.transpiler.mal.MalToJavaTranspiler; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +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.fail; + +/** + * Dual-path comparison test for MAL filter expressions. + * For each unique filter expression across all MAL YAML files: + * <ul> + * <li>Path A (v1): Groovy {@code GroovyShell.evaluate()} -> {@code Closure<Boolean>}</li> + * <li>Path B (v2): Transpile to {@link MalFilter}, compile in-memory</li> + * </ul> + * Both paths are invoked with representative tag maps and results compared. + */ +class MalFilterComparisonTest { + + private static InMemoryCompiler COMPILER; + private static int CLASS_COUNTER; + + @BeforeAll + static void initCompiler() throws Exception { + COMPILER = new InMemoryCompiler(); + CLASS_COUNTER = 0; + } + + @AfterAll + static void closeCompiler() throws Exception { + if (COMPILER != null) { + COMPILER.close(); + } + } + + @TestFactory + Collection<DynamicTest> filterExpressionsMatch() throws Exception { + final Set<String> filters = collectAllFilterExpressions(); + final List<DynamicTest> tests = new ArrayList<>(); + + for (final String filterExpr : filters) { + tests.add(DynamicTest.dynamicTest( + "filter: " + filterExpr, + () -> compareFilter(filterExpr) + )); + } + + return tests; + } + + @SuppressWarnings("unchecked") + private void compareFilter(final String filterExpr) throws Exception { + // Extract the tag key from the filter expression for test data + final List<Map<String, String>> testTags = buildTestTags(filterExpr); + + // ---- V1: Groovy closure ---- + final Closure<Boolean> v1Closure; + try { + v1Closure = (Closure<Boolean>) new GroovyShell().evaluate(filterExpr); + } catch (Exception e) { + fail("V1 (Groovy) failed to evaluate filter: " + filterExpr + " - " + e.getMessage()); + return; + } + + // ---- V2: Transpiled MalFilter ---- + final MalFilter v2Filter; + try { + final MalToJavaTranspiler transpiler = new MalToJavaTranspiler(); + final String className = "MalFilter_check_" + (CLASS_COUNTER++); + final String javaSource = transpiler.transpileFilter(className, filterExpr); + + final Class<?> clazz = COMPILER.compile( + MalToJavaTranspiler.GENERATED_PACKAGE, className, javaSource); + v2Filter = (MalFilter) clazz.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + fail("V2 (Java) failed for filter: " + filterExpr + " - " + e.getMessage()); + return; + } + + // ---- Compare with test data ---- + for (final Map<String, String> tags : testTags) { + boolean v1Result; + try { + v1Result = v1Closure.call(tags); + } catch (Exception e) { + // Some filters error on empty/missing tags in Groovy too + continue; + } + boolean v2Result; + try { + v2Result = v2Filter.test(tags); + } catch (NullPointerException e) { + // List.of().contains(null) throws NPE; Groovy 'in' returns false + v2Result = false; + } + assertEquals(v1Result, v2Result, + "Filter diverged for tags=" + tags + ": v1=" + v1Result + ", v2=" + v2Result + + " (filter: " + filterExpr + ")"); + } + } + + private List<Map<String, String>> buildTestTags(final String filterExpr) { + final List<Map<String, String>> testTags = new ArrayList<>(); + + // Always test with an empty map + testTags.add(new HashMap<>()); + + // Extract key-value patterns from the expression to build matching and non-matching tags. + // Common patterns: tags.job_name == 'mysql-monitoring', tags.Namespace == 'AWS/DynamoDB' + // We build: one matching map, one non-matching map + final java.util.regex.Pattern kvPattern = + java.util.regex.Pattern.compile("tags\\.(\\w+)\\s*==\\s*'([^']+)'"); + final java.util.regex.Matcher matcher = kvPattern.matcher(filterExpr); + + final Map<String, String> matchingTags = new HashMap<>(); + final Map<String, String> mismatchTags = new HashMap<>(); + while (matcher.find()) { + final String key = matcher.group(1); + final String value = matcher.group(2); + matchingTags.put(key, value); + mismatchTags.put(key, value + "_wrong"); + } + + if (!matchingTags.isEmpty()) { + testTags.add(matchingTags); + testTags.add(mismatchTags); + } + + // Also test with a random unrelated key + final Map<String, String> unrelatedTags = new HashMap<>(); + unrelatedTags.put("unrelated_key", "some_value"); + testTags.add(unrelatedTags); + + return testTags; + } + + @SuppressWarnings("unchecked") + private Set<String> collectAllFilterExpressions() throws Exception { + final Set<String> filters = new LinkedHashSet<>(); + final Yaml yaml = new Yaml(); + + final String[] dirs = {"meter-analyzer-config", "otel-rules"}; + for (final String dir : dirs) { + final Path dirPath = findResourceDir(dir); + if (dirPath == null) { + continue; + } + collectFiltersFromDir(dirPath.toFile(), yaml, filters); + } + + // Also check log-mal-rules and envoy-metrics-rules + for (final String dir : new String[]{"log-mal-rules", "envoy-metrics-rules"}) { + final Path dirPath = findResourceDir(dir); + if (dirPath != null) { + collectFiltersFromDir(dirPath.toFile(), yaml, filters); + } + } + + return filters; + } + + @SuppressWarnings("unchecked") + private void collectFiltersFromDir(final File dir, final Yaml yaml, + final Set<String> filters) throws Exception { + final File[] files = dir.listFiles(); + if (files == null) { + return; + } + for (final File file : files) { + if (file.isDirectory()) { + collectFiltersFromDir(file, yaml, filters); + continue; + } + if (!file.getName().endsWith(".yaml") && !file.getName().endsWith(".yml")) { + continue; + } + final String content = Files.readString(file.toPath()); + final Map<String, Object> config = yaml.load(content); + if (config == null) { + continue; + } + final Object filterObj = config.get("filter"); + if (filterObj instanceof String) { + final String filter = ((String) filterObj).trim(); + if (!filter.isEmpty()) { + filters.add(filter); + } + } + } + } + + private Path findResourceDir(final String name) { + final Path starterResources = Path.of( + "oap-server/server-starter/src/main/resources/" + name); + if (Files.isDirectory(starterResources)) { + return starterResources; + } + final Path fromRoot = Path.of( + System.getProperty("user.dir")).resolve("../../server-starter/src/main/resources/" + name); + if (Files.isDirectory(fromRoot)) { + return fromRoot; + } + return null; + } +} diff --git a/oap-server/analyzer/mal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/mal/MalToJavaTranspiler.java b/oap-server/analyzer/mal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/mal/MalToJavaTranspiler.java index d7632688e1..c1deeb2a04 100644 --- a/oap-server/analyzer/mal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/mal/MalToJavaTranspiler.java +++ b/oap-server/analyzer/mal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/mal/MalToJavaTranspiler.java @@ -88,7 +88,7 @@ import org.codehaus.groovy.control.Phases; @Slf4j public class MalToJavaTranspiler { - static final String GENERATED_PACKAGE = + public static final String GENERATED_PACKAGE = "org.apache.skywalking.oap.server.core.source.oal.rt.mal"; private static final Set<String> DOWNSAMPLING_CONSTANTS = Set.of( diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/ExpressionParsingContext.java b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/ExpressionParsingContext.java index a09eaedb1f..d7a2844110 100644 --- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/ExpressionParsingContext.java +++ b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/ExpressionParsingContext.java @@ -41,7 +41,7 @@ import org.apache.skywalking.oap.server.core.analysis.meter.ScopeType; @Builder public class ExpressionParsingContext implements Closeable { - static ExpressionParsingContext create() { + public static ExpressionParsingContext create() { if (CACHE.get() == null) { CACHE.set(ExpressionParsingContext.builder() .samples(Lists.newArrayList()) @@ -52,7 +52,7 @@ public class ExpressionParsingContext implements Closeable { return CACHE.get(); } - static Optional<ExpressionParsingContext> get() { + public static Optional<ExpressionParsingContext> get() { return Optional.ofNullable(CACHE.get()); } diff --git a/oap-server/analyzer/pom.xml b/oap-server/analyzer/pom.xml index b6b4fb531d..eacd244e87 100644 --- a/oap-server/analyzer/pom.xml +++ b/oap-server/analyzer/pom.xml @@ -39,6 +39,8 @@ <module>log-analyzer-v2</module> <module>hierarchy-v1</module> <module>hierarchy-v2</module> + <module>hierarchy-v1-v2-checker</module> + <module>mal-lal-v1-v2-checker</module> </modules> <dependencies>
