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:55:50 UTC
(skywalking) 05/27: Add v2 runtime modules for MAL and LAL with manifest-based class loading (Phase 4)
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 <wu...@foxmail.com>
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 <no...@anthropic.com>
---
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>