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 e9a6c9d54984f0577e9211ad12e8bb7fd165942e Author: Wu Sheng <[email protected]> AuthorDate: Sat Feb 28 15:58:56 2026 +0800 Add v2 runtime modules for MAL and LAL with manifest-based class loading (Phase 4) Introduces meter-analyzer-v2 and log-analyzer-v2 modules that provide same-FQCN replacement classes for DSL.java, Expression.java, and FilterExpression.java. The v2 classes load transpiled MalExpression/ MalFilter/LalExpression implementations from META-INF manifests via Class.forName() instead of Groovy GroovyShell/ExpandoMetaClass/ DelegatingScript. Uses maven-shade-plugin to overlay the upstream Groovy-dependent classes. Includes 7 unit tests. Co-Authored-By: Claude Opus 4.6 <[email protected]> --- oap-server/analyzer/log-analyzer-v2/pom.xml | 72 +++++++++++ .../skywalking/oap/log/analyzer/dsl/DSL.java | 142 +++++++++++++++++++++ .../skywalking/oap/log/analyzer/dsl/DSLV2Test.java | 60 +++++++++ oap-server/analyzer/meter-analyzer-v2/pom.xml | 78 +++++++++++ .../skywalking/oap/meter/analyzer/dsl/DSL.java | 116 +++++++++++++++++ .../oap/meter/analyzer/dsl/Expression.java | 92 +++++++++++++ .../oap/meter/analyzer/dsl/FilterExpression.java | 113 ++++++++++++++++ .../oap/meter/analyzer/dsl/DSLV2Test.java | 98 ++++++++++++++ oap-server/analyzer/pom.xml | 2 + 9 files changed, 773 insertions(+) diff --git a/oap-server/analyzer/log-analyzer-v2/pom.xml b/oap-server/analyzer/log-analyzer-v2/pom.xml new file mode 100644 index 0000000000..6456f8209f --- /dev/null +++ b/oap-server/analyzer/log-analyzer-v2/pom.xml @@ -0,0 +1,72 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ 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. + ~ + --> + +<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>analyzer</artifactId> + <groupId>org.apache.skywalking</groupId> + <version>${revision}</version> + </parent> + <modelVersion>4.0.0</modelVersion> + + <artifactId>log-analyzer-v2</artifactId> + <description>Pure Java LAL runtime that loads transpiled LalExpression classes from manifest instead of Groovy</description> + + <dependencies> + <dependency> + <groupId>org.apache.skywalking</groupId> + <artifactId>log-analyzer</artifactId> + <version>${project.version}</version> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-shade-plugin</artifactId> + <executions> + <execution> + <phase>package</phase> + <goals> + <goal>shade</goal> + </goals> + <configuration> + <createDependencyReducedPom>true</createDependencyReducedPom> + <artifactSet> + <includes> + <include>org.apache.skywalking:log-analyzer</include> + </includes> + </artifactSet> + <filters> + <filter> + <artifact>org.apache.skywalking:log-analyzer</artifact> + <excludes> + <exclude>org/apache/skywalking/oap/log/analyzer/dsl/DSL.class</exclude> + <exclude>org/apache/skywalking/oap/log/analyzer/dsl/DSL$*.class</exclude> + </excludes> + </filter> + </filters> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/oap-server/analyzer/log-analyzer-v2/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/DSL.java b/oap-server/analyzer/log-analyzer-v2/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/DSL.java new file mode 100644 index 0000000000..b3f4e4c977 --- /dev/null +++ b/oap-server/analyzer/log-analyzer-v2/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/DSL.java @@ -0,0 +1,142 @@ +/* + * 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.dsl; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.skywalking.oap.log.analyzer.dsl.spec.filter.FilterSpec; +import org.apache.skywalking.oap.log.analyzer.provider.LogAnalyzerModuleConfig; +import org.apache.skywalking.oap.server.library.module.ModuleManager; +import org.apache.skywalking.oap.server.library.module.ModuleStartException; + +/** + * Same-FQCN replacement for upstream LAL DSL. + * Loads pre-compiled {@link LalExpression} classes from lal-expressions.txt manifest + * (keyed by SHA-256 hash) instead of Groovy {@code GroovyShell} runtime compilation. + */ +@Slf4j +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class DSL { + private static final String MANIFEST_PATH = "META-INF/lal-expressions.txt"; + private static volatile Map<String, String> EXPRESSION_MAP; + private static final AtomicInteger LOADED_COUNT = new AtomicInteger(); + + private final LalExpression expression; + private final FilterSpec filterSpec; + private Binding binding; + + public static DSL of(final ModuleManager moduleManager, + final LogAnalyzerModuleConfig config, + final String dsl) throws ModuleStartException { + final Map<String, String> exprMap = loadManifest(); + final String dslHash = sha256(dsl); + final String className = exprMap.get(dslHash); + if (className == null) { + throw new ModuleStartException( + "Pre-compiled LAL expression not found for DSL hash: " + dslHash + + ". Available: " + exprMap.size() + " expressions."); + } + + try { + final Class<?> exprClass = Class.forName(className); + final LalExpression expression = (LalExpression) exprClass.getDeclaredConstructor().newInstance(); + final FilterSpec filterSpec = new FilterSpec(moduleManager, config); + final int count = LOADED_COUNT.incrementAndGet(); + log.debug("Loaded pre-compiled LAL expression [{}/{}]: {}", count, exprMap.size(), className); + return new DSL(expression, filterSpec); + } catch (ClassNotFoundException e) { + throw new ModuleStartException( + "Pre-compiled LAL expression class not found: " + className, e); + } catch (ReflectiveOperationException e) { + throw new ModuleStartException( + "Pre-compiled LAL expression instantiation failed: " + className, e); + } + } + + public void bind(final Binding binding) { + this.binding = binding; + this.filterSpec.bind(binding); + } + + public void evaluate() { + expression.execute(filterSpec, binding); + } + + private static Map<String, String> loadManifest() { + if (EXPRESSION_MAP != null) { + return EXPRESSION_MAP; + } + synchronized (DSL.class) { + if (EXPRESSION_MAP != null) { + return EXPRESSION_MAP; + } + final Map<String, String> map = new HashMap<>(); + try (InputStream is = DSL.class.getClassLoader().getResourceAsStream(MANIFEST_PATH)) { + if (is == null) { + log.warn("LAL expression manifest not found: {}", MANIFEST_PATH); + EXPRESSION_MAP = map; + return map; + } + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(is, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) { + continue; + } + final String[] parts = line.split("=", 2); + if (parts.length == 2) { + map.put(parts[0], parts[1]); + } + } + } + } catch (IOException e) { + throw new IllegalStateException("Failed to load LAL expression manifest", e); + } + log.info("Loaded {} pre-compiled LAL expressions from manifest", map.size()); + EXPRESSION_MAP = map; + return map; + } + } + + static String sha256(final String input) { + try { + final MessageDigest digest = MessageDigest.getInstance("SHA-256"); + final byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + final StringBuilder hex = new StringBuilder(); + for (final byte b : hash) { + hex.append(String.format("%02x", b)); + } + return hex.toString(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 not available", e); + } + } +} diff --git a/oap-server/analyzer/log-analyzer-v2/src/test/java/org/apache/skywalking/oap/log/analyzer/dsl/DSLV2Test.java b/oap-server/analyzer/log-analyzer-v2/src/test/java/org/apache/skywalking/oap/log/analyzer/dsl/DSLV2Test.java new file mode 100644 index 0000000000..a8f19d382e --- /dev/null +++ b/oap-server/analyzer/log-analyzer-v2/src/test/java/org/apache/skywalking/oap/log/analyzer/dsl/DSLV2Test.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.skywalking.oap.log.analyzer.dsl; + +import org.apache.skywalking.oap.server.library.module.ModuleStartException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class DSLV2Test { + + @Test + void ofThrowsWhenManifestMissing() { + // No META-INF/lal-expressions.txt on test classpath + assertThrows(ModuleStartException.class, + () -> DSL.of(null, null, "filter { json {} sink {} }")); + } + + @Test + void sha256Deterministic() { + final String input = "filter { json {} sink {} }"; + final String hash1 = DSL.sha256(input); + final String hash2 = DSL.sha256(input); + assertNotNull(hash1); + assertEquals(64, hash1.length()); + assertEquals(hash1, hash2); + } + + @Test + void sha256DifferentInputsDifferentHashes() { + final String hash1 = DSL.sha256("filter { json {} sink {} }"); + final String hash2 = DSL.sha256("filter { text {} sink {} }"); + assertNotNull(hash1); + assertNotNull(hash2); + assertNotEquals(hash1, hash2); + } + + private static void assertNotEquals(final String a, final String b) { + if (a.equals(b)) { + throw new AssertionError("Expected different values but got: " + a); + } + } +} diff --git a/oap-server/analyzer/meter-analyzer-v2/pom.xml b/oap-server/analyzer/meter-analyzer-v2/pom.xml new file mode 100644 index 0000000000..8bbcaa43ed --- /dev/null +++ b/oap-server/analyzer/meter-analyzer-v2/pom.xml @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ 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. + ~ + --> + +<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>analyzer</artifactId> + <groupId>org.apache.skywalking</groupId> + <version>${revision}</version> + </parent> + <modelVersion>4.0.0</modelVersion> + + <artifactId>meter-analyzer-v2</artifactId> + <description>Pure Java MAL runtime that loads transpiled MalExpression/MalFilter classes from manifests instead of Groovy</description> + + <dependencies> + <dependency> + <groupId>org.apache.skywalking</groupId> + <artifactId>meter-analyzer</artifactId> + <version>${project.version}</version> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-shade-plugin</artifactId> + <executions> + <execution> + <phase>package</phase> + <goals> + <goal>shade</goal> + </goals> + <configuration> + <createDependencyReducedPom>true</createDependencyReducedPom> + <artifactSet> + <includes> + <include>org.apache.skywalking:meter-analyzer</include> + </includes> + </artifactSet> + <filters> + <filter> + <artifact>org.apache.skywalking:meter-analyzer</artifact> + <excludes> + <exclude>org/apache/skywalking/oap/meter/analyzer/dsl/DSL.class</exclude> + <exclude>org/apache/skywalking/oap/meter/analyzer/dsl/DSL$*.class</exclude> + <exclude>org/apache/skywalking/oap/meter/analyzer/dsl/Expression.class</exclude> + <exclude>org/apache/skywalking/oap/meter/analyzer/dsl/Expression$*.class</exclude> + <exclude>org/apache/skywalking/oap/meter/analyzer/dsl/FilterExpression.class</exclude> + <exclude>org/apache/skywalking/oap/meter/analyzer/dsl/FilterExpression$*.class</exclude> + <exclude>org/apache/skywalking/oap/meter/analyzer/dsl/NumberClosure.class</exclude> + <exclude>org/apache/skywalking/oap/meter/analyzer/dsl/NumberClosure$*.class</exclude> + </excludes> + </filter> + </filters> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/oap-server/analyzer/meter-analyzer-v2/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/DSL.java b/oap-server/analyzer/meter-analyzer-v2/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/DSL.java new file mode 100644 index 0000000000..3098ac7672 --- /dev/null +++ b/oap-server/analyzer/meter-analyzer-v2/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/DSL.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.skywalking.oap.meter.analyzer.dsl; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import lombok.extern.slf4j.Slf4j; + +/** + * Same-FQCN replacement for upstream MAL DSL. + * Loads transpiled {@link MalExpression} classes from mal-expressions.txt manifest + * instead of Groovy {@code DelegatingScript} classes -- no Groovy runtime needed. + */ +@Slf4j +public final class DSL { + private static final String MANIFEST_PATH = "META-INF/mal-expressions.txt"; + private static volatile Map<String, String> SCRIPT_MAP; + private static final AtomicInteger LOADED_COUNT = new AtomicInteger(); + + /** + * Parse string literal to Expression object, which can be reused. + * + * @param metricName the name of metric defined in mal rule + * @param expression string literal represents the DSL expression. + * @return Expression object could be executed. + */ + public static Expression parse(final String metricName, final String expression) { + if (metricName == null) { + throw new UnsupportedOperationException( + "Init expressions (metricName=null) are not supported in v2 mode. " + + "All init expressions must be pre-compiled at build time."); + } + + final Map<String, String> scriptMap = loadManifest(); + final String className = scriptMap.get(metricName); + if (className == null) { + throw new IllegalStateException( + "Transpiled MAL expression not found for metric: " + metricName + + ". Available: " + scriptMap.size() + " expressions"); + } + + try { + final Class<?> exprClass = Class.forName(className); + final MalExpression malExpr = (MalExpression) exprClass.getDeclaredConstructor().newInstance(); + final int count = LOADED_COUNT.incrementAndGet(); + log.debug("Loaded transpiled MAL expression [{}/{}]: {}", count, scriptMap.size(), metricName); + return new Expression(metricName, expression, malExpr); + } catch (ClassNotFoundException e) { + throw new IllegalStateException( + "Transpiled MAL expression class not found: " + className, e); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException( + "Failed to instantiate transpiled MAL expression: " + className, e); + } + } + + private static Map<String, String> loadManifest() { + if (SCRIPT_MAP != null) { + return SCRIPT_MAP; + } + synchronized (DSL.class) { + if (SCRIPT_MAP != null) { + return SCRIPT_MAP; + } + final Map<String, String> map = new HashMap<>(); + try (InputStream is = DSL.class.getClassLoader().getResourceAsStream(MANIFEST_PATH)) { + if (is == null) { + log.warn("MAL expression manifest not found: {}", MANIFEST_PATH); + SCRIPT_MAP = map; + return map; + } + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(is, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) { + continue; + } + final String simpleName = line.substring(line.lastIndexOf('.') + 1); + if (simpleName.startsWith("MalExpr_")) { + final String metric = simpleName.substring("MalExpr_".length()); + map.put(metric, line); + } + } + } + } catch (IOException e) { + throw new IllegalStateException("Failed to load MAL expression manifest", e); + } + log.info("Loaded {} transpiled MAL expressions from manifest", map.size()); + SCRIPT_MAP = map; + return map; + } + } +} diff --git a/oap-server/analyzer/meter-analyzer-v2/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/Expression.java b/oap-server/analyzer/meter-analyzer-v2/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/Expression.java new file mode 100644 index 0000000000..cf2e208301 --- /dev/null +++ b/oap-server/analyzer/meter-analyzer-v2/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/Expression.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.skywalking.oap.meter.analyzer.dsl; + +import com.google.common.collect.ImmutableMap; +import java.util.Map; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +/** + * Same-FQCN replacement for upstream Expression. + * Wraps a transpiled {@link MalExpression} (pure Java) instead of a Groovy DelegatingScript. + * No ExpandoMetaClass, no propertyMissing(), no ThreadLocal sample repository. + */ +@Slf4j +@ToString(of = {"literal"}) +public class Expression { + + private final String metricName; + private final String literal; + private final MalExpression expression; + + public Expression(final String metricName, final String literal, final MalExpression expression) { + this.metricName = metricName; + this.literal = literal; + this.expression = expression; + } + + /** + * Parse the expression statically. + * + * @return Parsed context of the expression. + */ + public ExpressionParsingContext parse() { + try (ExpressionParsingContext ctx = ExpressionParsingContext.create()) { + final Result r = run(ImmutableMap.of()); + if (!r.isSuccess() && r.isThrowable()) { + throw new ExpressionParsingException( + "failed to parse expression: " + literal + ", error:" + r.getError()); + } + if (log.isDebugEnabled()) { + log.debug("\"{}\" is parsed", literal); + } + ctx.validate(literal); + return ctx; + } + } + + /** + * Run the expression with a data map. + * + * @param sampleFamilies a data map includes all of candidates to be analysis. + * @return The result of execution. + */ + public Result run(final Map<String, SampleFamily> sampleFamilies) { + try { + for (final SampleFamily s : sampleFamilies.values()) { + if (s != SampleFamily.EMPTY) { + s.context.setMetricName(metricName); + } + } + final SampleFamily sf = expression.run(sampleFamilies); + if (sf == SampleFamily.EMPTY) { + if (ExpressionParsingContext.get().isEmpty()) { + if (log.isDebugEnabled()) { + log.debug("result of {} is empty by \"{}\"", sampleFamilies, literal); + } + } + return Result.fail("Parsed result is an EMPTY sample family"); + } + return Result.success(sf); + } catch (Throwable t) { + log.error("failed to run \"{}\"", literal, t); + return Result.fail(t); + } + } +} diff --git a/oap-server/analyzer/meter-analyzer-v2/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/FilterExpression.java b/oap-server/analyzer/meter-analyzer-v2/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/FilterExpression.java new file mode 100644 index 0000000000..b92318d9d3 --- /dev/null +++ b/oap-server/analyzer/meter-analyzer-v2/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/FilterExpression.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.skywalking.oap.meter.analyzer.dsl; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +/** + * Same-FQCN replacement for upstream FilterExpression. + * Loads transpiled {@link MalFilter} classes from mal-filter-expressions.properties + * manifest instead of Groovy filter closures -- no Groovy runtime needed. + */ +@Slf4j +@ToString(of = {"literal"}) +public class FilterExpression { + private static final String MANIFEST_PATH = "META-INF/mal-filter-expressions.properties"; + private static volatile Map<String, String> FILTER_MAP; + private static final AtomicInteger LOADED_COUNT = new AtomicInteger(); + + private final String literal; + private final MalFilter malFilter; + + @SuppressWarnings("unchecked") + public FilterExpression(final String literal) { + this.literal = literal; + + final Map<String, String> filterMap = loadManifest(); + final String className = filterMap.get(literal); + if (className == null) { + throw new IllegalStateException( + "Transpiled MAL filter not found for: " + literal + + ". Available filters: " + filterMap.size()); + } + + try { + final Class<?> filterClass = Class.forName(className); + malFilter = (MalFilter) filterClass.getDeclaredConstructor().newInstance(); + final int count = LOADED_COUNT.incrementAndGet(); + log.debug("Loaded transpiled MAL filter [{}/{}]: {}", count, filterMap.size(), literal); + } catch (ClassNotFoundException e) { + throw new IllegalStateException( + "Transpiled MAL filter class not found: " + className, e); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException( + "Failed to instantiate transpiled MAL filter: " + className, e); + } + } + + public Map<String, SampleFamily> filter(final Map<String, SampleFamily> sampleFamilies) { + try { + final Map<String, SampleFamily> result = new HashMap<>(); + for (final Map.Entry<String, SampleFamily> entry : sampleFamilies.entrySet()) { + final SampleFamily afterFilter = entry.getValue().filter(malFilter::test); + if (!Objects.equals(afterFilter, SampleFamily.EMPTY)) { + result.put(entry.getKey(), afterFilter); + } + } + return result; + } catch (Throwable t) { + log.error("failed to run \"{}\"", literal, t); + } + return sampleFamilies; + } + + private static Map<String, String> loadManifest() { + if (FILTER_MAP != null) { + return FILTER_MAP; + } + synchronized (FilterExpression.class) { + if (FILTER_MAP != null) { + return FILTER_MAP; + } + final Map<String, String> map = new HashMap<>(); + try (InputStream is = FilterExpression.class.getClassLoader().getResourceAsStream(MANIFEST_PATH)) { + if (is == null) { + log.warn("MAL filter manifest not found: {}", MANIFEST_PATH); + FILTER_MAP = map; + return map; + } + final Properties props = new Properties(); + props.load(is); + props.forEach((k, v) -> map.put((String) k, (String) v)); + } catch (IOException e) { + throw new IllegalStateException("Failed to load MAL filter manifest", e); + } + log.info("Loaded {} transpiled MAL filters from manifest", map.size()); + FILTER_MAP = map; + return map; + } + } +} diff --git a/oap-server/analyzer/meter-analyzer-v2/src/test/java/org/apache/skywalking/oap/meter/analyzer/dsl/DSLV2Test.java b/oap-server/analyzer/meter-analyzer-v2/src/test/java/org/apache/skywalking/oap/meter/analyzer/dsl/DSLV2Test.java new file mode 100644 index 0000000000..d64847e771 --- /dev/null +++ b/oap-server/analyzer/meter-analyzer-v2/src/test/java/org/apache/skywalking/oap/meter/analyzer/dsl/DSLV2Test.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.skywalking.oap.meter.analyzer.dsl; + +import com.google.common.collect.ImmutableMap; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DSLV2Test { + + @Test + void parseRejectsNullMetricName() { + assertThrows(UnsupportedOperationException.class, () -> DSL.parse(null, "test_metric")); + } + + @Test + void parseThrowsWhenManifestMissing() { + assertThrows(IllegalStateException.class, () -> DSL.parse("nonexistent_metric", "some_expr")); + } + + @Test + void expressionRunWithMalExpression() { + final MalExpression simple = samples -> + samples.getOrDefault("test_metric", SampleFamily.EMPTY); + + final Expression expr = new Expression("test_metric", "test_metric", simple); + + // Run with empty map should return fail (EMPTY) + final Result emptyResult = expr.run(Map.of()); + assertNotNull(emptyResult); + assertFalse(emptyResult.isSuccess()); + + // Run with a real sample should return success + final Sample sample = Sample.builder() + .name("test_metric") + .labels(ImmutableMap.of("service", "svc1")) + .value(42.0) + .timestamp(System.currentTimeMillis()) + .build(); + final SampleFamily sf = SampleFamily.build(SampleFamily.RunningContext.instance(), sample); + final Map<String, SampleFamily> sampleMap = new HashMap<>(); + sampleMap.put("test_metric", sf); + + final Result result = expr.run(sampleMap); + assertNotNull(result); + assertTrue(result.isSuccess()); + assertEquals(sf, result.getData()); + } + + @Test + void filterExpressionWithMalFilter() { + final MalFilter filter = tags -> "svc1".equals(tags.get("service")); + + final Sample sample1 = Sample.builder() + .name("metric") + .labels(ImmutableMap.of("service", "svc1")) + .value(10.0) + .timestamp(System.currentTimeMillis()) + .build(); + final Sample sample2 = Sample.builder() + .name("metric") + .labels(ImmutableMap.of("service", "svc2")) + .value(20.0) + .timestamp(System.currentTimeMillis()) + .build(); + + final SampleFamily sf = SampleFamily.build( + SampleFamily.RunningContext.instance(), sample1, sample2); + + final SampleFamily filtered = sf.filter(filter::test); + assertNotNull(filtered); + assertTrue(filtered != SampleFamily.EMPTY); + assertEquals(1, filtered.samples.length); + assertEquals(10.0, filtered.samples[0].getValue()); + } +} diff --git a/oap-server/analyzer/pom.xml b/oap-server/analyzer/pom.xml index 1c2a04f34d..5053d0e628 100644 --- a/oap-server/analyzer/pom.xml +++ b/oap-server/analyzer/pom.xml @@ -35,6 +35,8 @@ <module>event-analyzer</module> <module>mal-transpiler</module> <module>lal-transpiler</module> + <module>meter-analyzer-v2</module> + <module>log-analyzer-v2</module> </modules> <dependencies>
