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:53 UTC
(skywalking) 08/27: Merge grammar and compiler modules into analyzer modules; wire MAL DSL to on-the-fly compilation
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 b5b4ee9123d92e2dfba04f30681b2f4867a69fe2
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Sun Mar 1 01:53:14 2026 +0800
Merge grammar and compiler modules into analyzer modules; wire MAL DSL to on-the-fly compilation
- Merge mal-grammar + mal-compiler into meter-analyzer
- Merge lal-grammar + lal-compiler into log-analyzer
- Merge hierarchy-rule-grammar + hierarchy-rule-compiler into hierarchy
- Remove 6 standalone modules (3 grammar + 3 compiler)
- Update DSL.java to compile MAL expressions on-the-fly via MALClassGenerator
instead of loading from non-existent manifest file
- Add varargs handling for tagEqual/tagNotEqual/tagMatch/tagNotMatch in
generated Javassist code (wrap String args in new String[]{})
- Update test/script-compiler checker POMs to reference merged module names
- Update CLAUDE.md files with merged file structure and paths
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
docs/en/academy/groovy-replacement-plan.md | 117 ++-
.../core/config/JavaHierarchyRuleProvider.java | 86 --
.../core/config/JavaHierarchyRuleProviderTest.java | 155 ---
oap-server/analyzer/hierarchy/CLAUDE.md | 113 ++
.../{hierarchy-v1-v2-checker => hierarchy}/pom.xml | 38 +-
.../hierarchy/rt/grammar/HierarchyRuleLexer.g4 | 105 ++
.../hierarchy/rt/grammar/HierarchyRuleParser.g4 | 135 +++
.../compiler/HierarchyRuleClassGenerator.java | 294 ++++++
.../core/config/compiler/HierarchyRuleModel.java | 260 +++++
.../config/compiler/HierarchyRuleScriptParser.java | 375 +++++++
.../compiler/rt/HierarchyRulePackageHolder.java} | 14 +-
.../compiler/HierarchyRuleClassGeneratorTest.java | 122 +++
.../compiler/HierarchyRuleScriptParserTest.java | 159 +++
.../server/transpiler/lal/LalToJavaTranspiler.java | 923 ----------------
.../transpiler/lal/LalToJavaTranspilerTest.java | 678 ------------
oap-server/analyzer/log-analyzer-v2/pom.xml | 72 --
.../skywalking/oap/log/analyzer/dsl/DSL.java | 142 ---
oap-server/analyzer/log-analyzer/CLAUDE.md | 128 +++
oap-server/analyzer/log-analyzer/pom.xml | 33 +-
.../apache/skywalking/lal/rt/grammar/LALLexer.g4 | 175 ++++
.../apache/skywalking/lal/rt/grammar/LALParser.g4 | 464 +++++++++
.../log/analyzer/compiler/LALClassGenerator.java | 627 +++++++++++
.../oap/log/analyzer/compiler/LALScriptModel.java | 459 ++++++++
.../oap/log/analyzer/compiler/LALScriptParser.java | 687 ++++++++++++
.../log/analyzer/compiler/rt/BindingAware.java} | 17 +-
.../compiler/rt/LalExpressionPackageHolder.java} | 14 +-
.../skywalking/oap/log/analyzer/dsl/Binding.java | 84 +-
.../skywalking/oap/log/analyzer/dsl/DSL.java | 161 +--
.../oap/log/analyzer/dsl/spec/AbstractSpec.java | 9 +-
.../analyzer/dsl/spec/extractor/ExtractorSpec.java | 95 --
.../log/analyzer/dsl/spec/filter/FilterSpec.java | 106 --
.../log/analyzer/dsl/spec/sink/SamplerSpec.java | 34 -
.../oap/log/analyzer/dsl/spec/sink/SinkSpec.java | 27 -
.../analyzer/compiler/LALClassGeneratorTest.java | 97 ++
.../log/analyzer/compiler/LALScriptParserTest.java | 192 ++++
.../skywalking/oap/log/analyzer/dsl/DSLV2Test.java | 0
.../server/transpiler/mal/MalToJavaTranspiler.java | 1099 --------------------
.../transpiler/mal/MalToJavaTranspilerTest.java | 904 ----------------
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-server/analyzer/meter-analyzer/CLAUDE.md | 100 ++
oap-server/analyzer/meter-analyzer/pom.xml | 32 +-
.../apache/skywalking/mal/rt/grammar/MALLexer.g4 | 111 ++
.../apache/skywalking/mal/rt/grammar/MALParser.g4 | 232 +++++
.../skywalking/oap/meter/analyzer/Analyzer.java | 6 +-
.../meter/analyzer/compiler/MALClassGenerator.java | 971 +++++++++++++++++
.../analyzer/compiler/MALExpressionModel.java | 480 +++++++++
.../meter/analyzer/compiler/MALScriptParser.java | 491 +++++++++
.../rt/MalExpressionPackageHolder.java} | 14 +-
.../skywalking/oap/meter/analyzer/dsl/DSL.java | 72 +-
.../oap/meter/analyzer/dsl/Expression.java | 113 +-
.../oap/meter/analyzer/dsl/ExpressionMetadata.java | 66 ++
.../oap/meter/analyzer/dsl/FilterExpression.java | 73 +-
.../oap/meter/analyzer/dsl/MalExpression.java | 9 +-
.../oap/meter/analyzer/dsl/SampleFamily.java | 158 +--
.../analyzer/compiler/MALClassGeneratorTest.java | 115 ++
.../analyzer/compiler/MALScriptParserTest.java | 238 +++++
.../oap/meter/analyzer/dsl/DSLV2Test.java | 42 +-
oap-server/analyzer/pom.xml | 13 +-
.../oap/query/graphql/resolver/PprofQuery.java | 2 +-
pom.xml | 1 +
.../hierarchy-v1-v2-checker/pom.xml | 10 +-
.../core/config/HierarchyRuleComparisonTest.java | 0
.../src/test/resources/hierarchy-definition.yml | 0
.../hierarchy-v1-with-groovy}/pom.xml | 4 +-
.../core/config/GroovyHierarchyRuleProvider.java | 0
.../script-compiler/lal-v1-with-groovy}/pom.xml | 13 +-
.../skywalking/oap/log/analyzer/dsl/Binding.java | 0
.../skywalking/oap/log/analyzer/dsl/DSL.java | 0
.../log/analyzer/dsl/LALPrecompiledExtension.java | 0
.../oap/log/analyzer/dsl/LalExpression.java | 12 +-
.../oap/log/analyzer/dsl/spec/AbstractSpec.java | 0
.../log/analyzer/dsl/spec/LALDelegatingScript.java | 0
.../analyzer/dsl/spec/extractor/ExtractorSpec.java | 0
.../extractor/sampledtrace/SampledTraceSpec.java | 105 ++
.../dsl/spec/extractor/slowsql/SlowSqlSpec.java | 57 +-
.../log/analyzer/dsl/spec/filter/FilterSpec.java | 0
.../dsl/spec/parser/AbstractParserSpec.java | 49 +
.../analyzer/dsl/spec/parser/JsonParserSpec.java | 40 +
.../analyzer/dsl/spec/parser/TextParserSpec.java | 57 +
.../analyzer/dsl/spec/parser/YamlParserSpec.java | 46 +
.../log/analyzer/dsl/spec/sink/SamplerSpec.java | 0
.../oap/log/analyzer/dsl/spec/sink/SinkSpec.java | 0
.../dsl/spec/sink/sampler/PossibilitySampler.java | 54 +
.../dsl/spec/sink/sampler/RateLimitingSampler.java | 107 ++
.../analyzer/dsl/spec/sink/sampler/Sampler.java | 30 +-
.../oap/log/analyzer/module/LogAnalyzerModule.java | 28 +-
.../oap/log/analyzer/provider/LALConfig.java | 18 +-
.../oap/log/analyzer/provider/LALConfigs.java | 77 ++
.../analyzer/provider/LogAnalyzerModuleConfig.java | 74 ++
.../provider/LogAnalyzerModuleProvider.java | 102 ++
.../provider/log/ILogAnalysisListenerManager.java | 25 +-
.../analyzer/provider/log/ILogAnalyzerService.java | 23 +-
.../oap/log/analyzer/provider/log/LogAnalyzer.java | 90 ++
.../provider/log/LogAnalyzerServiceImpl.java | 62 ++
.../provider/log/analyzer/LogAnalyzerFactory.java | 12 +-
.../provider/log/listener/LogAnalysisListener.java | 25 +-
.../log/listener/LogAnalysisListenerFactory.java | 17 +-
.../provider/log/listener/LogFilterListener.java | 96 ++
.../provider/log/listener/LogSinkListener.java | 25 +-
.../log/listener/LogSinkListenerFactory.java | 14 +-
.../provider/log/listener/RecordSinkListener.java | 178 ++++
.../provider/log/listener/TrafficSinkListener.java | 118 +++
...ywalking.oap.server.library.module.ModuleDefine | 5 +-
...alking.oap.server.library.module.ModuleProvider | 6 +-
.../oap/log/analyzer/dsl/DSLSecurityTest.java | 0
.../skywalking/oap/log/analyzer/dsl/DSLTest.java | 0
.../test/resources/log-mal-rules/placeholder.yaml | 0
.../org.mockito.plugins.MockMaker | 0
.../script-compiler}/mal-lal-v1-v2-checker/pom.xml | 19 +-
.../oap/server/checker/InMemoryCompiler.java | 0
.../oap/server/checker/lal/LalComparisonTest.java | 0
.../oap/server/checker/mal/MalComparisonTest.java | 0
.../checker/mal/MalFilterComparisonTest.java | 0
.../script-compiler/mal-v1-with-groovy}/pom.xml | 8 +-
.../skywalking/oap/meter/analyzer/Analyzer.java | 0
.../oap/meter/analyzer/MetricConvert.java | 130 +++
.../oap/meter/analyzer/MetricRuleConfig.java | 50 +-
.../skywalking/oap/meter/analyzer/dsl/DSL.java | 0
.../oap/meter/analyzer/dsl/DownsamplingType.java | 10 +-
.../EndpointEntityDescription.java | 44 +
.../dsl/EntityDescription/EntityDescription.java | 16 +-
.../InstanceEntityDescription.java | 48 +
.../ProcessEntityDescription.java | 49 +
.../ProcessRelationEntityDescription.java | 49 +
.../ServiceEntityDescription.java | 29 +-
.../ServiceRelationEntityDescription.java | 53 +
.../oap/meter/analyzer/dsl/Expression.java | 0
.../analyzer/dsl/ExpressionParsingContext.java | 0
.../analyzer/dsl/ExpressionParsingException.java | 12 +-
.../oap/meter/analyzer/dsl/FilterExpression.java | 0
.../oap/meter/analyzer/dsl/MalExpression.java | 0
.../oap/meter/analyzer/dsl/MalFilter.java | 8 +-
.../oap/meter/analyzer/dsl/NumberClosure.java | 0
.../skywalking/oap/meter/analyzer/dsl/Result.java | 82 ++
.../skywalking/oap/meter/analyzer/dsl/Sample.java | 60 ++
.../oap/meter/analyzer/dsl/SampleFamily.java | 0
.../meter/analyzer/dsl/SampleFamilyBuilder.java | 33 +-
.../meter/analyzer/dsl/SampleFamilyFunctions.java | 77 ++
.../meter/analyzer/dsl/counter/CounterWindow.java | 88 ++
.../oap/meter/analyzer/dsl/counter/ID.java | 22 +-
.../analyzer/dsl/registry/ProcessRegistry.java | 86 ++
.../meter/analyzer/dsl/tagOpt/K8sRetagType.java | 52 +
.../oap/meter/analyzer/dsl/tagOpt/Retag.java | 15 +-
.../oap/meter/analyzer/k8s/K8sInfoRegistry.java | 161 +++
.../prometheus/PrometheusMetricConverter.java | 152 +++
.../analyzer/prometheus/rule/MetricsRule.java | 21 +-
.../oap/meter/analyzer/prometheus/rule/Rule.java | 24 +-
.../oap/meter/analyzer/prometheus/rule/Rules.java | 120 +++
.../oap/meter/analyzer/MetricConvertTest.java | 0
.../oap/meter/analyzer/dsl/AggregationTest.java | 0
.../oap/meter/analyzer/dsl/AnalyzerTest.java | 0
.../oap/meter/analyzer/dsl/ArithmeticTest.java | 0
.../oap/meter/analyzer/dsl/BasicTest.java | 0
.../oap/meter/analyzer/dsl/DecorateTest.java | 0
.../meter/analyzer/dsl/ExpressionParsingTest.java | 0
.../oap/meter/analyzer/dsl/FilterTest.java | 0
.../oap/meter/analyzer/dsl/FunctionTest.java | 0
.../oap/meter/analyzer/dsl/IncreaseTest.java | 0
.../oap/meter/analyzer/dsl/K8sTagTest.java | 0
.../oap/meter/analyzer/dsl/ScopeTest.java | 0
.../oap/meter/analyzer/dsl/TagFilterTest.java | 0
.../oap/meter/analyzer/dsl/ValueFilterTest.java | 0
.../analyzer/dsl/counter/CounterWindowTest.java | 0
.../analyzer/dsl/rule/RuleLoaderFailTest.java | 0
.../meter/analyzer/dsl/rule/RuleLoaderTest.java | 0
.../analyzer/dsl/rule/RuleLoaderYAMLFailTest.java | 0
.../org.mockito.plugins.MockMaker | 0
.../resources/otel-rules/illegal-yaml/test.yml | 0
.../resources/otel-rules/single-file-case.yaml | 0
.../resources/otel-rules/test-folder/case1.yaml | 0
.../resources/otel-rules/test-folder/case2.yml | 0
.../resources/otel-rules/test-folder/case3.yaml | 0
.../test-folder/deeperFolder/caseUnReach.yaml | 0
.../resources/otel-rules/test-folder/empty.yaml | 0
.../hierarchy-v2 => test/script-compiler}/pom.xml | 21 +-
178 files changed, 10545 insertions(+), 5511 deletions(-)
diff --git a/docs/en/academy/groovy-replacement-plan.md b/docs/en/academy/groovy-replacement-plan.md
index 1018615038..f656a7b94d 100644
--- a/docs/en/academy/groovy-replacement-plan.md
+++ b/docs/en/academy/groovy-replacement-plan.md
@@ -606,6 +606,120 @@ v1 (Groovy) and v2 (Java) never coexist in the OAP runtime classpath. The `mal-l
7. **Remove `LALDelegatingScript.java`**: replaced by `LalExpression` interface
8. **Verify `server-core` has zero Groovy imports**: `HierarchyDefinitionService` and `HierarchyService` now use `BiFunction` only
+### Phase 8: Replace v2 Manifest Loading with Real Compilers (ANTLR4 + Javassist)
+
+Phase 7 completed: v2 modules are standalone (zero Groovy), v1 depends on v2. Currently, v2 loads transpiled classes via manifest files (`META-INF/mal-expressions.txt`, `META-INF/lal-expressions.txt`) that were pre-compiled at build time. This prevents on-demand config changes since MAL/LAL/hierarchy configs are in the final package and users may want to modify them.
+
+The goal is to replace this manifest-based approach with **real compilers** following the OAL pattern: ANTLR4 grammar -> parser -> model -> Javassist class generation -> listener notification. This enables runtime compilation when configs change.
+
+#### Module Renaming: Drop `-v2` Suffix
+
+Since v1 modules move to `test/script-compiler/`, the v2 modules become the primary ones and lose the `-v2` suffix:
+- `meter-analyzer-v2` -> `meter-analyzer` (package stays `o.a.s.oap.meter.analyzer`)
+- `log-analyzer-v2` -> `log-analyzer` (package stays `o.a.s.oap.log.analyzer`)
+- `hierarchy-v2` -> `hierarchy` (no v1 name conflict since v1 moves out)
+- `.v2.` sub-packages (`dsl.v2.DSL`, `dsl.v2.Binding`, etc.) merge back into parent packages (`dsl.DSL`, `dsl.Binding`)
+
+#### Target Module Structure
+
+```
+oap-server/
+ mal-grammar/ NEW — ANTLR4 grammar for MAL expressions
+ lal-grammar/ NEW — ANTLR4 grammar for LAL scripts
+ hierarchy-rule-grammar/ NEW — ANTLR4 grammar for hierarchy matching rules
+
+oap-server/analyzer/
+ agent-analyzer/ (stays)
+ event-analyzer/ (stays)
+ meter-analyzer/ (renamed from meter-analyzer-v2, runtime MAL, calls mal-compiler)
+ log-analyzer/ (renamed from log-analyzer-v2, runtime LAL, calls lal-compiler)
+ hierarchy/ (renamed from hierarchy-v2, calls hierarchy-rule-compiler)
+ mal-compiler/ NEW — MAL expression compiler engine
+ lal-compiler/ NEW — LAL script compiler engine
+ hierarchy-rule-compiler/ NEW — hierarchy rule compiler engine
+
+test/script-compiler/ NEW — aggregator for v1/transpiler/checker (not in dist)
+ mal-groovy/ <- meter-analyzer v1 (Groovy)
+ lal-groovy/ <- log-analyzer v1 (Groovy)
+ hierarchy-groovy/ <- hierarchy-v1 (Groovy)
+ mal-transpiler/ <- mal-transpiler
+ lal-transpiler/ <- lal-transpiler
+ mal-lal-v1-v2-checker/ <- mal-lal-v1-v2-checker
+ hierarchy-v1-v2-checker/ <- hierarchy-v1-v2-checker
+```
+
+#### Generated Class Grouping by Config File Name
+
+MAL metrics come from YAML config files (e.g., `oap.yaml`, `spring-micrometer.yaml`). Each file contains multiple `metricsRules`. The compiler groups generated classes by source file name.
+
+- **MAL Compiler API**: `MALCompilerEngine.compile(configFileName, MetricRuleConfig)` -> grouped by file
+- **Generated class naming**: `rt.<configFile>.MalExpr_<metricName>`, e.g., `rt.oap.MalExpr_instance_jvm_cpu`
+- **LAL Compiler API**: `LALCompilerEngine.compile(configFileName, List<LALConfig>)` -> grouped by file
+- **Generated class naming**: `rt.<configFile>.LalExpr_<ruleName>`
+
+#### Eliminate `ExpressionParsingContext` ThreadLocal (MAL only)
+
+The current MAL `run()` method serves dual purposes controlled by a ThreadLocal:
+1. **Parse phase** (startup): `ExpressionParsingContext` ThreadLocal is set, `run()` is called with an empty map to discover which metric names the expression references
+2. **Runtime phase** (every ingestion cycle): ThreadLocal is not set, `run()` computes the actual result
+
+This is eliminated by extracting metadata statically. The `MalExpression` interface gains a `metadata()` method:
+
+```java
+public interface MalExpression {
+ /** Pure computation. No side effects. */
+ SampleFamily run(Map<String, SampleFamily> samples);
+
+ /** Compile-time metadata -- sample names, scope, downsampling, etc. */
+ ExpressionMetadata metadata();
+}
+```
+
+The ANTLR4 compiler extracts all metadata from the parse tree at compile time:
+
+| Metadata | Extracted from |
+|--|--|
+| `sampleNames` | Bare identifiers (metric references) |
+| `scopeType` | Terminal method: `.service()`, `.instance()`, `.endpoint()` |
+| `downsampling` | Aggregation arg: `AVG`, `SUM`, `MAX`, etc. |
+| `percentiles` | `.percentile()` call arguments |
+| `isHistogram` | Presence of `.histogram()` in chain |
+
+Generated class emits metadata as static fields:
+
+```java
+public class MalExpr_instance_jvm_cpu implements MalExpression {
+ private static final ExpressionMetadata METADATA = new ExpressionMetadata(
+ List.of("instance_jvm_cpu"), // sampleNames
+ ScopeType.SERVICE_INSTANCE, // from .service()/instance() call
+ DownsamplingType.AVG // from aggregation
+ );
+
+ @Override
+ public ExpressionMetadata metadata() { return METADATA; }
+
+ @Override
+ public SampleFamily run(Map<String, SampleFamily> samples) {
+ return ((SampleFamily) samples.getOrDefault("instance_jvm_cpu", SampleFamily.EMPTY))
+ .sum(List.of("service", "instance"));
+ }
+}
+```
+
+Result: `run()` is pure computation, `metadata()` is static facts, `ExpressionParsingContext` and its ThreadLocal are deleted. No dry run with empty map at startup.
+
+LAL and hierarchy do **not** have this problem -- LAL passes `Binding` explicitly as a parameter, hierarchy rules are stateless lambdas.
+
+#### Implementation Sub-Phases
+
+- 8.1: Rename modules (drop -v2 suffix, flatten .v2. sub-packages)
+- 8.2: Grammar modules (ANTLR4 .g4 files)
+- 8.3: Compiler model + parser (no code gen)
+- 8.4: Javassist code generation (including static `ExpressionMetadata` on generated MAL classes)
+- 8.5: Engine integration (wire compilers into renamed modules, delete `ExpressionParsingContext`)
+- 8.6: Move v1/transpiler/checker to test/script-compiler/
+- 8.7: Cleanup (remove manifests, verify zero Groovy)
+
---
## 6. What Gets Removed from Runtime
@@ -880,7 +994,6 @@ New MAL/LAL YAML rules added to `server-starter/src/main/resources/` are automat
| LAL YAML files processed | 8 |
| LAL rules transpiled | 10 (6 unique after SHA-256 dedup) |
| Hierarchy rules replaced | 4 |
-| Hierarchy rules replaced | 4 |
| Total generated Java classes | ~1,289 |
| Comparison test assertions | 1,300+ (MAL: 1,281, LAL: 19, hierarchy: 4 rules x multiple service pairs) |
| Lines of transpiler code (MAL) | ~1,230 |
@@ -909,4 +1022,4 @@ New MAL/LAL YAML rules added to `server-starter/src/main/resources/` are automat
4. **Phase 5**: Hierarchy v1/v2 module split -- refactor `server-core` to remove Groovy, create `hierarchy-v1/` (Groovy, checker-only) and `hierarchy-v2/` (Java lambdas, runtime)
5. **Phase 6**: Build comparison test suites -- `mal-lal-v1-v2-checker/` AND `hierarchy-v1-v2-checker/`
6. **Phase 7**: Switch `server-starter` from v1 to v2 for all three subsystems (MAL, LAL, hierarchy), remove Groovy from runtime classpath
-7. **Eventually**: Remove `mal-lal-v1`, `hierarchy-v1`, and all checker modules once community confidence is established
+7. **Phase 8**: Replace v2 manifest-based class loading with real ANTLR4 + Javassist compilers following the OAL pattern, enabling runtime compilation when configs change. Rename modules (drop `-v2` suffix), move v1/transpiler/checker to `test/script-compiler/`.
diff --git a/oap-server/analyzer/hierarchy-v2/src/main/java/org/apache/skywalking/oap/server/core/config/JavaHierarchyRuleProvider.java b/oap-server/analyzer/hierarchy-v2/src/main/java/org/apache/skywalking/oap/server/core/config/JavaHierarchyRuleProvider.java
deleted file mode 100644
index d85bdb99c7..0000000000
--- a/oap-server/analyzer/hierarchy-v2/src/main/java/org/apache/skywalking/oap/server/core/config/JavaHierarchyRuleProvider.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * 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.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-import java.util.function.BiFunction;
-import org.apache.skywalking.oap.server.core.query.type.Service;
-
-/**
- * Pure Java hierarchy rule provider. Contains a static registry of all known
- * hierarchy matching rules as Java lambdas. Zero Groovy dependency.
- *
- * <p>Rule names must match those in hierarchy-definition.yml auto-matching-rules section.
- * Unknown rule names fail fast at startup with IllegalArgumentException.
- */
-public final class JavaHierarchyRuleProvider implements HierarchyDefinitionService.HierarchyRuleProvider {
-
- private static final Map<String, BiFunction<Service, Service, Boolean>> RULE_REGISTRY;
-
- static {
- RULE_REGISTRY = new HashMap<>();
-
- // name: { (u, l) -> u.name == l.name }
- RULE_REGISTRY.put("name",
- (u, l) -> Objects.equals(u.getName(), l.getName()));
-
- // short-name: { (u, l) -> u.shortName == l.shortName }
- RULE_REGISTRY.put("short-name",
- (u, l) -> Objects.equals(u.getShortName(), l.getShortName()));
-
- // lower-short-name-remove-ns:
- // { (u, l) -> { if(l.shortName.lastIndexOf('.') > 0)
- // return u.shortName == l.shortName.substring(0, l.shortName.lastIndexOf('.'));
- // return false; } }
- RULE_REGISTRY.put("lower-short-name-remove-ns", (u, l) -> {
- final String sn = l.getShortName();
- final int dot = sn.lastIndexOf('.');
- return dot > 0 && Objects.equals(u.getShortName(), sn.substring(0, dot));
- });
-
- // 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; } }
- RULE_REGISTRY.put("lower-short-name-with-fqdn", (u, l) -> {
- final String sn = u.getShortName();
- final int colon = sn.lastIndexOf(':');
- return colon > 0 && Objects.equals(
- sn.substring(0, colon),
- l.getShortName() + ".svc.cluster.local");
- });
- }
-
- @Override
- public Map<String, BiFunction<Service, Service, Boolean>> buildRules(
- final Map<String, String> ruleExpressions) {
- final Map<String, BiFunction<Service, Service, Boolean>> rules = new HashMap<>();
- ruleExpressions.forEach((name, expression) -> {
- final BiFunction<Service, Service, Boolean> fn = RULE_REGISTRY.get(name);
- if (fn == null) {
- throw new IllegalArgumentException(
- "Unknown hierarchy matching rule: " + name
- + ". Known rules: " + RULE_REGISTRY.keySet());
- }
- rules.put(name, fn);
- });
- return rules;
- }
-}
diff --git a/oap-server/analyzer/hierarchy-v2/src/test/java/org/apache/skywalking/oap/server/core/config/JavaHierarchyRuleProviderTest.java b/oap-server/analyzer/hierarchy-v2/src/test/java/org/apache/skywalking/oap/server/core/config/JavaHierarchyRuleProviderTest.java
deleted file mode 100644
index 699d3af4aa..0000000000
--- a/oap-server/analyzer/hierarchy-v2/src/test/java/org/apache/skywalking/oap/server/core/config/JavaHierarchyRuleProviderTest.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * 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.util.Map;
-import java.util.function.BiFunction;
-import org.apache.skywalking.oap.server.core.query.type.Service;
-import org.junit.jupiter.api.BeforeEach;
-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.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-class JavaHierarchyRuleProviderTest {
-
- private JavaHierarchyRuleProvider provider;
-
- @BeforeEach
- void setUp() {
- provider = new JavaHierarchyRuleProvider();
- }
-
- private Service svc(final String name, final String shortName) {
- final Service s = new Service();
- s.setName(name);
- s.setShortName(shortName);
- return s;
- }
-
- // ---- name rule ----
-
- @Test
- void nameRuleMatches() {
- final Map<String, BiFunction<Service, Service, Boolean>> rules =
- provider.buildRules(Map.of("name", "{ (u, l) -> u.name == l.name }"));
- assertTrue(rules.get("name").apply(svc("svc", "svc"), svc("svc", "svc")));
- }
-
- @Test
- void nameRuleDoesNotMatch() {
- final Map<String, BiFunction<Service, Service, Boolean>> rules =
- provider.buildRules(Map.of("name", "ignored"));
- assertFalse(rules.get("name").apply(svc("svc", "svc"), svc("other", "other")));
- }
-
- // ---- short-name rule ----
-
- @Test
- void shortNameRuleMatches() {
- final Map<String, BiFunction<Service, Service, Boolean>> rules =
- provider.buildRules(Map.of("short-name", "ignored"));
- assertTrue(rules.get("short-name").apply(svc("a", "svc"), svc("b", "svc")));
- }
-
- @Test
- void shortNameRuleDoesNotMatch() {
- final Map<String, BiFunction<Service, Service, Boolean>> rules =
- provider.buildRules(Map.of("short-name", "ignored"));
- assertFalse(rules.get("short-name").apply(svc("a", "svc1"), svc("b", "svc2")));
- }
-
- // ---- lower-short-name-remove-ns rule ----
-
- @Test
- void lowerShortNameRemoveNsMatches() {
- final Map<String, BiFunction<Service, Service, Boolean>> rules =
- provider.buildRules(Map.of("lower-short-name-remove-ns", "ignored"));
- // l.shortName = "svc.namespace", u.shortName = "svc"
- assertTrue(rules.get("lower-short-name-remove-ns")
- .apply(svc("a", "svc"), svc("b", "svc.namespace")));
- }
-
- @Test
- void lowerShortNameRemoveNsNoDot() {
- final Map<String, BiFunction<Service, Service, Boolean>> rules =
- provider.buildRules(Map.of("lower-short-name-remove-ns", "ignored"));
- assertFalse(rules.get("lower-short-name-remove-ns")
- .apply(svc("a", "svc"), svc("b", "svc")));
- }
-
- @Test
- void lowerShortNameRemoveNsMismatch() {
- final Map<String, BiFunction<Service, Service, Boolean>> rules =
- provider.buildRules(Map.of("lower-short-name-remove-ns", "ignored"));
- assertFalse(rules.get("lower-short-name-remove-ns")
- .apply(svc("a", "other"), svc("b", "svc.namespace")));
- }
-
- // ---- lower-short-name-with-fqdn rule ----
-
- @Test
- void lowerShortNameWithFqdnMatches() {
- final Map<String, BiFunction<Service, Service, Boolean>> rules =
- provider.buildRules(Map.of("lower-short-name-with-fqdn", "ignored"));
- // u.shortName = "db:3306", l.shortName = "db" -> "db" == "db.svc.cluster.local"? no
- // u.shortName = "db:3306", l.shortName should match: u prefix = "db", l + fqdn = "db.svc.cluster.local"
- assertTrue(rules.get("lower-short-name-with-fqdn")
- .apply(svc("a", "db.svc.cluster.local:3306"), svc("b", "db")));
- }
-
- @Test
- void lowerShortNameWithFqdnNoColon() {
- final Map<String, BiFunction<Service, Service, Boolean>> rules =
- provider.buildRules(Map.of("lower-short-name-with-fqdn", "ignored"));
- assertFalse(rules.get("lower-short-name-with-fqdn")
- .apply(svc("a", "db"), svc("b", "db")));
- }
-
- @Test
- void lowerShortNameWithFqdnWrongSuffix() {
- final Map<String, BiFunction<Service, Service, Boolean>> rules =
- provider.buildRules(Map.of("lower-short-name-with-fqdn", "ignored"));
- assertFalse(rules.get("lower-short-name-with-fqdn")
- .apply(svc("a", "db:3306"), svc("b", "other")));
- }
-
- // ---- unknown rule ----
-
- @Test
- void unknownRuleThrows() {
- assertThrows(IllegalArgumentException.class,
- () -> provider.buildRules(Map.of("unknown-rule", "ignored")));
- }
-
- // ---- builds all 4 rules ----
-
- @Test
- void buildsAllFourRules() {
- final Map<String, BiFunction<Service, Service, Boolean>> rules =
- provider.buildRules(Map.of(
- "name", "ignored",
- "short-name", "ignored",
- "lower-short-name-remove-ns", "ignored",
- "lower-short-name-with-fqdn", "ignored"
- ));
- assertEquals(4, rules.size());
- }
-}
diff --git a/oap-server/analyzer/hierarchy/CLAUDE.md b/oap-server/analyzer/hierarchy/CLAUDE.md
new file mode 100644
index 0000000000..f2bf5d1d9a
--- /dev/null
+++ b/oap-server/analyzer/hierarchy/CLAUDE.md
@@ -0,0 +1,113 @@
+# Hierarchy Rule Compiler
+
+Compiles hierarchy matching rule expressions into `BiFunction<Service, Service, Boolean>` implementation classes at runtime using ANTLR4 parsing and Javassist bytecode generation.
+
+## Compilation Workflow
+
+```
+Rule expression string (e.g., "{ (u, l) -> u.name == l.name }")
+ → HierarchyRuleScriptParser.parse(expression) [ANTLR4 lexer/parser → visitor]
+ → HierarchyRuleModel (immutable AST)
+ → HierarchyRuleClassGenerator.compile(ruleName, expression)
+ 1. classPool.makeClass() — create class implementing BiFunction
+ 2. generateApplyMethod(model) — emit Java source for apply(Object, Object)
+ 3. ctClass.toClass(HierarchyRulePackageHolder.class) — load via package anchor
+ → BiFunction<Service, Service, Boolean> instance
+```
+
+The generated class implements:
+```java
+Object apply(Object arg0, Object arg1)
+ // cast internally to Service and returns Boolean
+```
+
+No separate consumer/closure classes are needed — hierarchy rules are simple enough to compile into a single method body.
+
+## File Structure
+
+```
+oap-server/analyzer/hierarchy/
+ src/main/antlr4/.../HierarchyRuleLexer.g4 — ANTLR4 lexer grammar
+ src/main/antlr4/.../HierarchyRuleParser.g4 — ANTLR4 parser grammar
+
+ src/main/java/.../compiler/
+ HierarchyRuleScriptParser.java — ANTLR4 facade: expression → AST
+ HierarchyRuleModel.java — Immutable AST model classes
+ HierarchyRuleClassGenerator.java — Javassist code generator
+ rt/
+ HierarchyRulePackageHolder.java — Class loading anchor (empty marker)
+
+ src/test/java/.../compiler/
+ HierarchyRuleScriptParserTest.java — 5 parser tests
+ HierarchyRuleClassGeneratorTest.java — 4 generator tests
+```
+
+## Package & Class Naming
+
+| Component | Package / Name |
+|-----------|---------------|
+| Parser/Model/Generator | `org.apache.skywalking.oap.server.core.config.compiler` |
+| Generated classes | `org.apache.skywalking.oap.server.core.config.compiler.rt.HierarchyRule_<N>` |
+| Package holder | `org.apache.skywalking.oap.server.core.config.compiler.rt.HierarchyRulePackageHolder` |
+| Service type | `org.apache.skywalking.oap.server.core.query.type.Service` (in server-core) |
+
+`<N>` is a global `AtomicInteger` counter.
+
+## Code Generation Details
+
+**Field access mapping**: Property access in expressions maps to getter methods:
+- `u.name` → `u.getName()`
+- `l.shortName` → `l.getShortName()`
+- Generic: `x.foo` → `x.getFoo()`
+
+**Comparison operators**: `==` and `!=` use `java.util.Objects.equals()`. Numeric comparisons (`>`, `<`, `>=`, `<=`) emit direct operators.
+
+**Method chains**: `l.shortName.substring(0, l.shortName.lastIndexOf("."))` generates chained Java method calls directly.
+
+## Example
+
+**Input**: `{ (u, l) -> u.name == l.name }`
+
+**Generated `apply()` method**:
+```java
+public Object apply(Object arg0, Object arg1) {
+ Service u = (Service) arg0;
+ Service l = (Service) arg1;
+ return Boolean.valueOf(java.util.Objects.equals(u.getName(), l.getName()));
+}
+```
+
+**Input with block body**: `{ (u, l) -> { if (l.shortName.lastIndexOf(".") > 0) { return u.name == l.shortName.substring(0, l.shortName.lastIndexOf(".")); } return false; } }`
+
+**Generated `apply()` method**:
+```java
+public Object apply(Object arg0, Object arg1) {
+ Service u = (Service) arg0;
+ Service l = (Service) arg1;
+ if (l.getShortName().lastIndexOf(".") > 0) {
+ return Boolean.valueOf(
+ java.util.Objects.equals(
+ u.getName(),
+ l.getShortName().substring(0, l.getShortName().lastIndexOf("."))));
+ }
+ return Boolean.valueOf(false);
+}
+```
+
+## Rule Patterns
+
+Four rule types are defined in `hierarchy-definition.yml`:
+
+| Rule Name | Expression Pattern |
+|-----------|-------------------|
+| `name` | `{ (u, l) -> u.name == l.name }` |
+| `short-name` | `{ (u, l) -> u.shortName == l.shortName }` |
+| `lower-short-name-remove-namespace` | `{ (u, l) -> { if (l.shortName.lastIndexOf(".") > 0) { return u.name == l.shortName.substring(0, l.shortName.lastIndexOf(".")); } return false; } }` |
+| `lower-short-name-with-fqdn` | `{ (u, l) -> u.shortName == l.shortName.concat("." + u.shortName) }` |
+
+## Dependencies
+
+Grammar, compiler, and runtime are merged into this module:
+- ANTLR4 grammar → generates lexer/parser at build time
+- `server-core` — `Service` type
+- `javassist` — bytecode generation
diff --git a/oap-server/analyzer/hierarchy-v1-v2-checker/pom.xml b/oap-server/analyzer/hierarchy/pom.xml
similarity index 66%
copy from oap-server/analyzer/hierarchy-v1-v2-checker/pom.xml
copy to oap-server/analyzer/hierarchy/pom.xml
index a663a0fd1a..04adce9ed0 100644
--- a/oap-server/analyzer/hierarchy-v1-v2-checker/pom.xml
+++ b/oap-server/analyzer/hierarchy/pom.xml
@@ -25,27 +25,41 @@
</parent>
<modelVersion>4.0.0</modelVersion>
- <artifactId>hierarchy-v1-v2-checker</artifactId>
- <description>Dual-path comparison tests: Groovy hierarchy rules (v1) vs Java hierarchy rules (v2)</description>
+ <artifactId>hierarchy</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.skywalking</groupId>
- <artifactId>hierarchy-v1</artifactId>
+ <artifactId>server-core</artifactId>
<version>${project.version}</version>
- <scope>test</scope>
</dependency>
<dependency>
- <groupId>org.apache.skywalking</groupId>
- <artifactId>hierarchy-v2</artifactId>
- <version>${project.version}</version>
- <scope>test</scope>
+ <groupId>org.antlr</groupId>
+ <artifactId>antlr4-runtime</artifactId>
</dependency>
<dependency>
- <groupId>org.apache.skywalking</groupId>
- <artifactId>server-core</artifactId>
- <version>${project.version}</version>
- <scope>test</scope>
+ <groupId>org.javassist</groupId>
+ <artifactId>javassist</artifactId>
</dependency>
</dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.antlr</groupId>
+ <artifactId>antlr4-maven-plugin</artifactId>
+ <configuration>
+ <visitor>true</visitor>
+ </configuration>
+ <executions>
+ <execution>
+ <id>antlr</id>
+ <goals>
+ <goal>antlr4</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
</project>
diff --git a/oap-server/analyzer/hierarchy/src/main/antlr4/org/apache/skywalking/hierarchy/rt/grammar/HierarchyRuleLexer.g4 b/oap-server/analyzer/hierarchy/src/main/antlr4/org/apache/skywalking/hierarchy/rt/grammar/HierarchyRuleLexer.g4
new file mode 100644
index 0000000000..2cd2989992
--- /dev/null
+++ b/oap-server/analyzer/hierarchy/src/main/antlr4/org/apache/skywalking/hierarchy/rt/grammar/HierarchyRuleLexer.g4
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ *
+ */
+
+// Hierarchy Rule matching expression lexer
+//
+// Covers expressions like:
+// { (u, l) -> u.name == l.name }
+// { (u, l) -> { if(l.shortName.lastIndexOf('.') > 0) return u.shortName == l.shortName.substring(0, l.shortName.lastIndexOf('.')); return false; } }
+lexer grammar HierarchyRuleLexer;
+
+@Header {package org.apache.skywalking.hierarchy.rt.grammar;}
+
+// Keywords
+IF: 'if';
+ELSE: 'else';
+RETURN: 'return';
+TRUE: 'true';
+FALSE: 'false';
+
+// Comparison and logical operators
+DEQ: '==';
+NEQ: '!=';
+AND: '&&';
+OR: '||';
+NOT: '!';
+GT: '>';
+LT: '<';
+GTE: '>=';
+LTE: '<=';
+
+// Delimiters
+DOT: '.';
+COMMA: ',';
+SEMI: ';';
+L_PAREN: '(';
+R_PAREN: ')';
+L_BRACE: '{';
+R_BRACE: '}';
+ARROW: '->';
+
+// Arithmetic (for substring index arguments)
+PLUS: '+';
+MINUS: '-';
+
+// Literals
+NUMBER
+ : Digit+
+ ;
+
+STRING
+ : '\'' (~['\\\r\n] | EscapeSequence)* '\''
+ | '"' (~["\\\r\n] | EscapeSequence)* '"'
+ ;
+
+// Comments
+LINE_COMMENT
+ : '//' ~[\r\n]* -> channel(HIDDEN)
+ ;
+
+BLOCK_COMMENT
+ : '/*' .*? '*/' -> channel(HIDDEN)
+ ;
+
+// Whitespace
+WS
+ : [ \t\r\n]+ -> channel(HIDDEN)
+ ;
+
+// Identifiers
+IDENTIFIER
+ : Letter LetterOrDigit*
+ ;
+
+// Fragments
+fragment EscapeSequence
+ : '\\' [btnfr"'\\]
+ ;
+
+fragment Digit
+ : [0-9]
+ ;
+
+fragment Letter
+ : [a-zA-Z_]
+ ;
+
+fragment LetterOrDigit
+ : Letter
+ | [0-9]
+ ;
diff --git a/oap-server/analyzer/hierarchy/src/main/antlr4/org/apache/skywalking/hierarchy/rt/grammar/HierarchyRuleParser.g4 b/oap-server/analyzer/hierarchy/src/main/antlr4/org/apache/skywalking/hierarchy/rt/grammar/HierarchyRuleParser.g4
new file mode 100644
index 0000000000..7d30c016c1
--- /dev/null
+++ b/oap-server/analyzer/hierarchy/src/main/antlr4/org/apache/skywalking/hierarchy/rt/grammar/HierarchyRuleParser.g4
@@ -0,0 +1,135 @@
+/*
+ * 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.
+ *
+ */
+
+// Hierarchy Rule matching expression parser
+//
+// Parses expressions from hierarchy-definition.yml auto-matching-rules:
+// name: "{ (u, l) -> u.name == l.name }"
+// short-name: "{ (u, l) -> u.shortName == l.shortName }"
+// lower-short-name-remove-ns:
+// "{ (u, l) -> { if(l.shortName.lastIndexOf('.') > 0) return u.shortName == l.shortName.substring(0, l.shortName.lastIndexOf('.')); return false; } }"
+// 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; } }"
+parser grammar HierarchyRuleParser;
+
+@Header {package org.apache.skywalking.hierarchy.rt.grammar;}
+
+options { tokenVocab=HierarchyRuleLexer; }
+
+// ==================== Top-level ====================
+
+// { (u, l) -> body }
+matchingRule
+ : L_BRACE L_PAREN param COMMA param R_PAREN ARROW ruleBody R_BRACE EOF
+ ;
+
+param
+ : IDENTIFIER
+ ;
+
+ruleBody
+ : simpleExpression // u.name == l.name
+ | blockBody // { if(...) ...; return false; }
+ ;
+
+// ==================== Block body ====================
+
+blockBody
+ : L_BRACE statement+ R_BRACE
+ ;
+
+statement
+ : ifStatement
+ | returnStatement
+ ;
+
+ifStatement
+ : IF L_PAREN condition R_PAREN
+ (returnStatement | blockBody)
+ (ELSE IF L_PAREN condition R_PAREN
+ (returnStatement | blockBody)
+ )*
+ (ELSE
+ (returnStatement | blockBody)
+ )?
+ ;
+
+returnStatement
+ : RETURN returnValue SEMI?
+ ;
+
+returnValue
+ : ruleExpr DEQ ruleExpr # returnComparison
+ | ruleExpr NEQ ruleExpr # returnNeqComparison
+ | ruleExpr # returnExpr
+ ;
+
+// ==================== Conditions ====================
+
+condition
+ : condition AND condition # condAnd
+ | condition OR condition # condOr
+ | NOT condition # condNot
+ | L_PAREN condition R_PAREN # condParen
+ | ruleExpr DEQ ruleExpr # condEq
+ | ruleExpr NEQ ruleExpr # condNeq
+ | ruleExpr GT ruleExpr # condGt
+ | ruleExpr LT ruleExpr # condLt
+ | ruleExpr GTE ruleExpr # condGte
+ | ruleExpr LTE ruleExpr # condLte
+ | ruleExpr # condExpr
+ ;
+
+// ==================== Expressions ====================
+
+simpleExpression
+ : ruleExpr DEQ ruleExpr
+ | ruleExpr NEQ ruleExpr
+ ;
+
+ruleExpr
+ : ruleExpr PLUS ruleExpr # exprAdd
+ | ruleExpr MINUS ruleExpr # exprSub
+ | ruleExprPrimary # exprPrimary
+ ;
+
+ruleExprPrimary
+ : methodChain # exprMethodChain
+ | STRING # exprString
+ | NUMBER # exprNumber
+ | TRUE # exprTrue
+ | FALSE # exprFalse
+ ;
+
+// ==================== Method chains ====================
+
+// u.name, l.shortName, l.shortName.lastIndexOf('.'),
+// u.shortName.substring(0, l.shortName.lastIndexOf(':'))
+// l.shortName.concat('.svc.cluster.local')
+methodChain
+ : IDENTIFIER (DOT chainSegment)+
+ ;
+
+chainSegment
+ : IDENTIFIER L_PAREN argList? R_PAREN # chainMethodCall
+ | IDENTIFIER # chainFieldAccess
+ ;
+
+argList
+ : ruleExpr (COMMA ruleExpr)*
+ ;
diff --git a/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/compiler/HierarchyRuleClassGenerator.java b/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/compiler/HierarchyRuleClassGenerator.java
new file mode 100644
index 0000000000..754bffa278
--- /dev/null
+++ b/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/compiler/HierarchyRuleClassGenerator.java
@@ -0,0 +1,294 @@
+/*
+ * 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.compiler;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiFunction;
+import javassist.ClassPool;
+import javassist.CtClass;
+import javassist.CtNewConstructor;
+import javassist.CtNewMethod;
+import org.apache.skywalking.oap.server.core.config.compiler.rt.HierarchyRulePackageHolder;
+import org.apache.skywalking.oap.server.core.query.type.Service;
+
+/**
+ * Generates {@link BiFunction BiFunction<Service, Service, Boolean>} implementation classes
+ * from {@link HierarchyRuleModel} AST using Javassist bytecode generation.
+ */
+public final class HierarchyRuleClassGenerator {
+
+ private static final AtomicInteger CLASS_COUNTER = new AtomicInteger(0);
+
+ private static final String PACKAGE_PREFIX =
+ "org.apache.skywalking.oap.server.core.config.compiler.rt.";
+
+ private final ClassPool classPool;
+
+ public HierarchyRuleClassGenerator() {
+ this(ClassPool.getDefault());
+ }
+
+ public HierarchyRuleClassGenerator(final ClassPool classPool) {
+ this.classPool = classPool;
+ }
+
+ /**
+ * Compiles a hierarchy rule expression into a BiFunction class.
+ *
+ * @param ruleName the rule name (e.g., "name", "short-name")
+ * @param expression the rule expression string
+ * @return a BiFunction that matches two Service objects
+ */
+ @SuppressWarnings("unchecked")
+ public BiFunction<Service, Service, Boolean> compile(
+ final String ruleName, final String expression) throws Exception {
+ final HierarchyRuleModel model = HierarchyRuleScriptParser.parse(expression);
+ final String className = PACKAGE_PREFIX + "HierarchyRule_"
+ + CLASS_COUNTER.getAndIncrement();
+
+ final CtClass ctClass = classPool.makeClass(className);
+ ctClass.addInterface(classPool.get("java.util.function.BiFunction"));
+
+ ctClass.addConstructor(CtNewConstructor.defaultConstructor(ctClass));
+
+ final String applyBody = generateApplyMethod(model);
+ ctClass.addMethod(CtNewMethod.make(applyBody, ctClass));
+
+ final Class<?> clazz = ctClass.toClass(HierarchyRulePackageHolder.class);
+ ctClass.detach();
+ return (BiFunction<Service, Service, Boolean>) clazz.getDeclaredConstructor().newInstance();
+ }
+
+ private String generateApplyMethod(final HierarchyRuleModel model) {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("public Object apply(Object arg0, Object arg1) {\n");
+ sb.append(" org.apache.skywalking.oap.server.core.query.type.Service ");
+ sb.append(model.getUpperParam()).append(" = (org.apache.skywalking.oap.server.core.query.type.Service) arg0;\n");
+ sb.append(" org.apache.skywalking.oap.server.core.query.type.Service ");
+ sb.append(model.getLowerParam()).append(" = (org.apache.skywalking.oap.server.core.query.type.Service) arg1;\n");
+
+ generateRuleBody(sb, model.getBody());
+
+ sb.append("}\n");
+ return sb.toString();
+ }
+
+ private void generateRuleBody(final StringBuilder sb,
+ final HierarchyRuleModel.RuleBody body) {
+ if (body instanceof HierarchyRuleModel.SimpleComparison) {
+ final HierarchyRuleModel.SimpleComparison cmp =
+ (HierarchyRuleModel.SimpleComparison) body;
+ sb.append(" return Boolean.valueOf(");
+ generateComparison(sb, cmp.getLeft(), cmp.getOp(), cmp.getRight());
+ sb.append(");\n");
+ } else if (body instanceof HierarchyRuleModel.BlockBody) {
+ final HierarchyRuleModel.BlockBody block = (HierarchyRuleModel.BlockBody) body;
+ for (final HierarchyRuleModel.Statement stmt : block.getStatements()) {
+ generateStatement(sb, stmt);
+ }
+ }
+ }
+
+ private void generateStatement(final StringBuilder sb,
+ final HierarchyRuleModel.Statement stmt) {
+ if (stmt instanceof HierarchyRuleModel.IfStatement) {
+ generateIfStatement(sb, (HierarchyRuleModel.IfStatement) stmt);
+ } else if (stmt instanceof HierarchyRuleModel.ReturnStatement) {
+ generateReturnStatement(sb, (HierarchyRuleModel.ReturnStatement) stmt);
+ }
+ }
+
+ private void generateIfStatement(final StringBuilder sb,
+ final HierarchyRuleModel.IfStatement ifStmt) {
+ sb.append(" if (");
+ generateCondition(sb, ifStmt.getCondition());
+ sb.append(") {\n");
+ for (final HierarchyRuleModel.Statement s : ifStmt.getThenBranch()) {
+ generateStatement(sb, s);
+ }
+ sb.append(" }\n");
+ if (ifStmt.getElseBranch() != null && !ifStmt.getElseBranch().isEmpty()) {
+ sb.append(" else {\n");
+ for (final HierarchyRuleModel.Statement s : ifStmt.getElseBranch()) {
+ generateStatement(sb, s);
+ }
+ sb.append(" }\n");
+ }
+ }
+
+ private void generateReturnStatement(final StringBuilder sb,
+ final HierarchyRuleModel.ReturnStatement retStmt) {
+ final HierarchyRuleModel.Expr expr = retStmt.getValue();
+ if (expr instanceof HierarchyRuleModel.SimpleComparison) {
+ final HierarchyRuleModel.SimpleComparison cmp =
+ (HierarchyRuleModel.SimpleComparison) expr;
+ sb.append(" return Boolean.valueOf(");
+ generateComparison(sb, cmp.getLeft(), cmp.getOp(), cmp.getRight());
+ sb.append(");\n");
+ } else if (expr instanceof HierarchyRuleModel.BoolLiteralExpr) {
+ sb.append(" return Boolean.valueOf(")
+ .append(((HierarchyRuleModel.BoolLiteralExpr) expr).isValue())
+ .append(");\n");
+ } else {
+ sb.append(" return Boolean.valueOf(");
+ generateExpr(sb, expr);
+ sb.append(" != null);\n");
+ }
+ }
+
+ private void generateComparison(final StringBuilder sb,
+ final HierarchyRuleModel.Expr left,
+ final HierarchyRuleModel.CompareOp op,
+ final HierarchyRuleModel.Expr right) {
+ switch (op) {
+ case EQ:
+ sb.append("java.util.Objects.equals(");
+ generateExpr(sb, left);
+ sb.append(", ");
+ generateExpr(sb, right);
+ sb.append(")");
+ break;
+ case NEQ:
+ sb.append("!java.util.Objects.equals(");
+ generateExpr(sb, left);
+ sb.append(", ");
+ generateExpr(sb, right);
+ sb.append(")");
+ break;
+ case GT:
+ generateExpr(sb, left);
+ sb.append(" > ");
+ generateExpr(sb, right);
+ break;
+ case LT:
+ generateExpr(sb, left);
+ sb.append(" < ");
+ generateExpr(sb, right);
+ break;
+ case GTE:
+ generateExpr(sb, left);
+ sb.append(" >= ");
+ generateExpr(sb, right);
+ break;
+ case LTE:
+ generateExpr(sb, left);
+ sb.append(" <= ");
+ generateExpr(sb, right);
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported comparison op: " + op);
+ }
+ }
+
+ private void generateCondition(final StringBuilder sb,
+ final HierarchyRuleModel.Condition cond) {
+ if (cond instanceof HierarchyRuleModel.ComparisonCondition) {
+ final HierarchyRuleModel.ComparisonCondition cc =
+ (HierarchyRuleModel.ComparisonCondition) cond;
+ generateComparison(sb, cc.getLeft(), cc.getOp(), cc.getRight());
+ } else if (cond instanceof HierarchyRuleModel.LogicalCondition) {
+ final HierarchyRuleModel.LogicalCondition lc =
+ (HierarchyRuleModel.LogicalCondition) cond;
+ sb.append("(");
+ generateCondition(sb, lc.getLeft());
+ sb.append(lc.getOp() == HierarchyRuleModel.LogicalOp.AND ? " && " : " || ");
+ generateCondition(sb, lc.getRight());
+ sb.append(")");
+ } else if (cond instanceof HierarchyRuleModel.NotCondition) {
+ sb.append("!(");
+ generateCondition(sb, ((HierarchyRuleModel.NotCondition) cond).getInner());
+ sb.append(")");
+ } else if (cond instanceof HierarchyRuleModel.ExprCondition) {
+ generateExpr(sb, ((HierarchyRuleModel.ExprCondition) cond).getExpr());
+ }
+ }
+
+ private void generateExpr(final StringBuilder sb,
+ final HierarchyRuleModel.Expr expr) {
+ if (expr instanceof HierarchyRuleModel.MethodChainExpr) {
+ generateMethodChainExpr(sb, (HierarchyRuleModel.MethodChainExpr) expr);
+ } else if (expr instanceof HierarchyRuleModel.StringLiteralExpr) {
+ sb.append('"')
+ .append(escapeJava(((HierarchyRuleModel.StringLiteralExpr) expr).getValue()))
+ .append('"');
+ } else if (expr instanceof HierarchyRuleModel.NumberLiteralExpr) {
+ sb.append(((HierarchyRuleModel.NumberLiteralExpr) expr).getValue());
+ } else if (expr instanceof HierarchyRuleModel.BoolLiteralExpr) {
+ sb.append(((HierarchyRuleModel.BoolLiteralExpr) expr).isValue());
+ } else if (expr instanceof HierarchyRuleModel.BinaryExpr) {
+ final HierarchyRuleModel.BinaryExpr bin = (HierarchyRuleModel.BinaryExpr) expr;
+ generateExpr(sb, bin.getLeft());
+ sb.append(bin.getOp() == HierarchyRuleModel.ArithmeticOp.ADD ? " + " : " - ");
+ generateExpr(sb, bin.getRight());
+ } else if (expr instanceof HierarchyRuleModel.SimpleComparison) {
+ final HierarchyRuleModel.SimpleComparison cmp =
+ (HierarchyRuleModel.SimpleComparison) expr;
+ generateComparison(sb, cmp.getLeft(), cmp.getOp(), cmp.getRight());
+ }
+ }
+
+ private void generateMethodChainExpr(final StringBuilder sb,
+ final HierarchyRuleModel.MethodChainExpr expr) {
+ sb.append(expr.getTarget());
+ for (final HierarchyRuleModel.ChainSegment seg : expr.getSegments()) {
+ sb.append('.');
+ if (seg instanceof HierarchyRuleModel.FieldAccess) {
+ final String fieldName = ((HierarchyRuleModel.FieldAccess) seg).getName();
+ sb.append(toGetter(fieldName)).append("()");
+ } else if (seg instanceof HierarchyRuleModel.MethodCallSegment) {
+ final HierarchyRuleModel.MethodCallSegment mc =
+ (HierarchyRuleModel.MethodCallSegment) seg;
+ sb.append(mc.getName()).append('(');
+ final List<HierarchyRuleModel.Expr> args = mc.getArguments();
+ for (int i = 0; i < args.size(); i++) {
+ if (i > 0) {
+ sb.append(", ");
+ }
+ generateExpr(sb, args.get(i));
+ }
+ sb.append(')');
+ }
+ }
+ }
+
+ private static String toGetter(final String fieldName) {
+ if ("name".equals(fieldName)) {
+ return "getName";
+ } else if ("shortName".equals(fieldName)) {
+ return "getShortName";
+ }
+ return "get" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
+ }
+
+ private static String escapeJava(final String s) {
+ return s.replace("\\", "\\\\")
+ .replace("\"", "\\\"")
+ .replace("\n", "\\n")
+ .replace("\r", "\\r")
+ .replace("\t", "\\t");
+ }
+
+ /**
+ * Generates the Java source body of the apply method for debugging/testing.
+ */
+ public String generateSource(final String expression) {
+ final HierarchyRuleModel model = HierarchyRuleScriptParser.parse(expression);
+ return generateApplyMethod(model);
+ }
+}
diff --git a/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/compiler/HierarchyRuleModel.java b/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/compiler/HierarchyRuleModel.java
new file mode 100644
index 0000000000..d9ecc4d707
--- /dev/null
+++ b/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/compiler/HierarchyRuleModel.java
@@ -0,0 +1,260 @@
+/*
+ * 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.compiler;
+
+import java.util.Collections;
+import java.util.List;
+import lombok.Getter;
+
+/**
+ * Immutable AST model for hierarchy matching rule expressions.
+ * Represents parsed expressions like:
+ * <pre>
+ * { (u, l) -> u.name == l.name }
+ * { (u, l) -> { if(l.shortName.lastIndexOf('.') > 0) return ...; return false; } }
+ * </pre>
+ */
+@Getter
+public final class HierarchyRuleModel {
+ private final String upperParam;
+ private final String lowerParam;
+ private final RuleBody body;
+
+ private HierarchyRuleModel(final String upperParam, final String lowerParam, final RuleBody body) {
+ this.upperParam = upperParam;
+ this.lowerParam = lowerParam;
+ this.body = body;
+ }
+
+ public static HierarchyRuleModel of(final String upperParam, final String lowerParam, final RuleBody body) {
+ return new HierarchyRuleModel(upperParam, lowerParam, body);
+ }
+
+ /**
+ * Rule body — either a simple comparison or a block with if/return statements.
+ */
+ public interface RuleBody {
+ }
+
+ /**
+ * Simple comparison body: {@code u.name == l.name}
+ */
+ @Getter
+ public static final class SimpleComparison implements RuleBody, Expr {
+ private final Expr left;
+ private final CompareOp op;
+ private final Expr right;
+
+ public SimpleComparison(final Expr left, final CompareOp op, final Expr right) {
+ this.left = left;
+ this.op = op;
+ this.right = right;
+ }
+ }
+
+ /**
+ * Block body with multiple statements: {@code { if(...) return ...; return false; }}
+ */
+ @Getter
+ public static final class BlockBody implements RuleBody {
+ private final List<Statement> statements;
+
+ public BlockBody(final List<Statement> statements) {
+ this.statements = Collections.unmodifiableList(statements);
+ }
+ }
+
+ // ==================== Statements ====================
+
+ public interface Statement {
+ }
+
+ @Getter
+ public static final class IfStatement implements Statement {
+ private final Condition condition;
+ private final List<Statement> thenBranch;
+ private final List<Statement> elseBranch;
+
+ public IfStatement(final Condition condition,
+ final List<Statement> thenBranch,
+ final List<Statement> elseBranch) {
+ this.condition = condition;
+ this.thenBranch = Collections.unmodifiableList(thenBranch);
+ this.elseBranch = elseBranch != null
+ ? Collections.unmodifiableList(elseBranch) : Collections.emptyList();
+ }
+ }
+
+ @Getter
+ public static final class ReturnStatement implements Statement {
+ private final Expr value;
+
+ public ReturnStatement(final Expr value) {
+ this.value = value;
+ }
+ }
+
+ // ==================== Conditions ====================
+
+ public interface Condition {
+ }
+
+ @Getter
+ public static final class ComparisonCondition implements Condition {
+ private final Expr left;
+ private final CompareOp op;
+ private final Expr right;
+
+ public ComparisonCondition(final Expr left, final CompareOp op, final Expr right) {
+ this.left = left;
+ this.op = op;
+ this.right = right;
+ }
+ }
+
+ @Getter
+ public static final class LogicalCondition implements Condition {
+ private final Condition left;
+ private final LogicalOp op;
+ private final Condition right;
+
+ public LogicalCondition(final Condition left, final LogicalOp op, final Condition right) {
+ this.left = left;
+ this.op = op;
+ this.right = right;
+ }
+ }
+
+ @Getter
+ public static final class NotCondition implements Condition {
+ private final Condition inner;
+
+ public NotCondition(final Condition inner) {
+ this.inner = inner;
+ }
+ }
+
+ @Getter
+ public static final class ExprCondition implements Condition {
+ private final Expr expr;
+
+ public ExprCondition(final Expr expr) {
+ this.expr = expr;
+ }
+ }
+
+ // ==================== Expressions ====================
+
+ public interface Expr {
+ }
+
+ /**
+ * Method chain: {@code u.name}, {@code l.shortName.lastIndexOf('.')},
+ * {@code u.shortName.substring(0, l.shortName.lastIndexOf(':'))}
+ */
+ @Getter
+ public static final class MethodChainExpr implements Expr {
+ private final String target;
+ private final List<ChainSegment> segments;
+
+ public MethodChainExpr(final String target, final List<ChainSegment> segments) {
+ this.target = target;
+ this.segments = Collections.unmodifiableList(segments);
+ }
+ }
+
+ @Getter
+ public static final class StringLiteralExpr implements Expr {
+ private final String value;
+
+ public StringLiteralExpr(final String value) {
+ this.value = value;
+ }
+ }
+
+ @Getter
+ public static final class NumberLiteralExpr implements Expr {
+ private final long value;
+
+ public NumberLiteralExpr(final long value) {
+ this.value = value;
+ }
+ }
+
+ @Getter
+ public static final class BoolLiteralExpr implements Expr {
+ private final boolean value;
+
+ public BoolLiteralExpr(final boolean value) {
+ this.value = value;
+ }
+ }
+
+ @Getter
+ public static final class BinaryExpr implements Expr {
+ private final Expr left;
+ private final ArithmeticOp op;
+ private final Expr right;
+
+ public BinaryExpr(final Expr left, final ArithmeticOp op, final Expr right) {
+ this.left = left;
+ this.op = op;
+ this.right = right;
+ }
+ }
+
+ // ==================== Chain segments ====================
+
+ public interface ChainSegment {
+ String getName();
+ }
+
+ @Getter
+ public static final class FieldAccess implements ChainSegment {
+ private final String name;
+
+ public FieldAccess(final String name) {
+ this.name = name;
+ }
+ }
+
+ @Getter
+ public static final class MethodCallSegment implements ChainSegment {
+ private final String name;
+ private final List<Expr> arguments;
+
+ public MethodCallSegment(final String name, final List<Expr> arguments) {
+ this.name = name;
+ this.arguments = Collections.unmodifiableList(arguments);
+ }
+ }
+
+ // ==================== Enums ====================
+
+ public enum CompareOp {
+ EQ, NEQ, GT, LT, GTE, LTE
+ }
+
+ public enum LogicalOp {
+ AND, OR
+ }
+
+ public enum ArithmeticOp {
+ ADD, SUB
+ }
+}
diff --git a/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/compiler/HierarchyRuleScriptParser.java b/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/compiler/HierarchyRuleScriptParser.java
new file mode 100644
index 0000000000..84dd3db2e2
--- /dev/null
+++ b/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/compiler/HierarchyRuleScriptParser.java
@@ -0,0 +1,375 @@
+/*
+ * 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.compiler;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.antlr.v4.runtime.BaseErrorListener;
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.antlr.v4.runtime.RecognitionException;
+import org.antlr.v4.runtime.Recognizer;
+import org.apache.skywalking.hierarchy.rt.grammar.HierarchyRuleLexer;
+import org.apache.skywalking.hierarchy.rt.grammar.HierarchyRuleParser;
+import org.apache.skywalking.hierarchy.rt.grammar.HierarchyRuleParserBaseVisitor;
+
+/**
+ * Facade: parses hierarchy rule expression strings into {@link HierarchyRuleModel}.
+ *
+ * <pre>
+ * HierarchyRuleModel model = HierarchyRuleScriptParser.parse(
+ * "{ (u, l) -> u.name == l.name }");
+ * </pre>
+ */
+public final class HierarchyRuleScriptParser {
+
+ private HierarchyRuleScriptParser() {
+ }
+
+ public static HierarchyRuleModel parse(final String expression) {
+ final HierarchyRuleLexer lexer = new HierarchyRuleLexer(
+ CharStreams.fromString(expression));
+ final CommonTokenStream tokens = new CommonTokenStream(lexer);
+ final HierarchyRuleParser parser = new HierarchyRuleParser(tokens);
+
+ final List<String> errors = new ArrayList<>();
+ parser.removeErrorListeners();
+ parser.addErrorListener(new BaseErrorListener() {
+ @Override
+ public void syntaxError(final Recognizer<?, ?> recognizer,
+ final Object offendingSymbol,
+ final int line,
+ final int charPositionInLine,
+ final String msg,
+ final RecognitionException e) {
+ errors.add(line + ":" + charPositionInLine + " " + msg);
+ }
+ });
+
+ final HierarchyRuleParser.MatchingRuleContext tree = parser.matchingRule();
+ if (!errors.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Hierarchy rule parsing failed: " + String.join("; ", errors)
+ + " in expression: " + expression);
+ }
+
+ return new HierarchyRuleModelVisitor().visit(tree);
+ }
+
+ /**
+ * Visitor that transforms the ANTLR4 parse tree into {@link HierarchyRuleModel}.
+ */
+ private static final class HierarchyRuleModelVisitor
+ extends HierarchyRuleParserBaseVisitor<HierarchyRuleModel> {
+
+ @Override
+ public HierarchyRuleModel visitMatchingRule(final HierarchyRuleParser.MatchingRuleContext ctx) {
+ final String upperParam = ctx.param(0).getText();
+ final String lowerParam = ctx.param(1).getText();
+ final HierarchyRuleModel.RuleBody body = convertRuleBody(ctx.ruleBody());
+ return HierarchyRuleModel.of(upperParam, lowerParam, body);
+ }
+
+ private HierarchyRuleModel.RuleBody convertRuleBody(
+ final HierarchyRuleParser.RuleBodyContext ctx) {
+ if (ctx.simpleExpression() != null) {
+ return convertSimpleExpression(ctx.simpleExpression());
+ }
+ return convertBlockBody(ctx.blockBody());
+ }
+
+ private HierarchyRuleModel.SimpleComparison convertSimpleExpression(
+ final HierarchyRuleParser.SimpleExpressionContext ctx) {
+ final HierarchyRuleModel.Expr left = new ExprVisitor().visitRuleExpr(ctx.ruleExpr(0));
+ final HierarchyRuleModel.Expr right = new ExprVisitor().visitRuleExpr(ctx.ruleExpr(1));
+ final HierarchyRuleModel.CompareOp op = ctx.DEQ() != null
+ ? HierarchyRuleModel.CompareOp.EQ : HierarchyRuleModel.CompareOp.NEQ;
+ return new HierarchyRuleModel.SimpleComparison(left, op, right);
+ }
+
+ private HierarchyRuleModel.BlockBody convertBlockBody(
+ final HierarchyRuleParser.BlockBodyContext ctx) {
+ final List<HierarchyRuleModel.Statement> stmts = new ArrayList<>();
+ for (final HierarchyRuleParser.StatementContext stmtCtx : ctx.statement()) {
+ stmts.add(convertStatement(stmtCtx));
+ }
+ return new HierarchyRuleModel.BlockBody(stmts);
+ }
+
+ private HierarchyRuleModel.Statement convertStatement(
+ final HierarchyRuleParser.StatementContext ctx) {
+ if (ctx.ifStatement() != null) {
+ return convertIfStatement(ctx.ifStatement());
+ }
+ return convertReturnStatement(ctx.returnStatement());
+ }
+
+ private HierarchyRuleModel.IfStatement convertIfStatement(
+ final HierarchyRuleParser.IfStatementContext ctx) {
+ final HierarchyRuleModel.Condition condition =
+ new ConditionVisitor().visit(ctx.condition(0));
+
+ final List<HierarchyRuleModel.Statement> thenBranch = new ArrayList<>();
+ if (ctx.returnStatement(0) != null) {
+ thenBranch.add(convertReturnStatement(ctx.returnStatement(0)));
+ } else if (ctx.blockBody(0) != null) {
+ thenBranch.addAll(convertBlockBody(ctx.blockBody(0)).getStatements());
+ }
+
+ final List<HierarchyRuleModel.Statement> elseBranch = new ArrayList<>();
+ // Handle else-if and else branches
+ final int condCount = ctx.condition().size();
+ final int retCount = ctx.returnStatement().size();
+ final int blockCount = ctx.blockBody().size();
+
+ // If there are more conditions (else if branches)
+ if (condCount > 1 || retCount > 1 || blockCount > 1) {
+ // Simplification: flatten else-if into the else branch
+ // For the current hierarchy rules, we don't have else-if patterns
+ // so this handles the basic else case
+ if (retCount > 1) {
+ elseBranch.add(convertReturnStatement(ctx.returnStatement(retCount - 1)));
+ } else if (blockCount > 1) {
+ elseBranch.addAll(
+ convertBlockBody(ctx.blockBody(blockCount - 1)).getStatements());
+ }
+ }
+
+ return new HierarchyRuleModel.IfStatement(condition, thenBranch, elseBranch);
+ }
+
+ private HierarchyRuleModel.ReturnStatement convertReturnStatement(
+ final HierarchyRuleParser.ReturnStatementContext ctx) {
+ final HierarchyRuleParser.ReturnValueContext rv = ctx.returnValue();
+ if (rv instanceof HierarchyRuleParser.ReturnComparisonContext) {
+ final HierarchyRuleParser.ReturnComparisonContext rc =
+ (HierarchyRuleParser.ReturnComparisonContext) rv;
+ final ExprVisitor ev = new ExprVisitor();
+ final HierarchyRuleModel.SimpleComparison comp =
+ new HierarchyRuleModel.SimpleComparison(
+ ev.visitRuleExpr(rc.ruleExpr(0)),
+ HierarchyRuleModel.CompareOp.EQ,
+ ev.visitRuleExpr(rc.ruleExpr(1)));
+ return new HierarchyRuleModel.ReturnStatement(comp);
+ }
+ if (rv instanceof HierarchyRuleParser.ReturnNeqComparisonContext) {
+ final HierarchyRuleParser.ReturnNeqComparisonContext rnc =
+ (HierarchyRuleParser.ReturnNeqComparisonContext) rv;
+ final ExprVisitor ev = new ExprVisitor();
+ final HierarchyRuleModel.SimpleComparison comp =
+ new HierarchyRuleModel.SimpleComparison(
+ ev.visitRuleExpr(rnc.ruleExpr(0)),
+ HierarchyRuleModel.CompareOp.NEQ,
+ ev.visitRuleExpr(rnc.ruleExpr(1)));
+ return new HierarchyRuleModel.ReturnStatement(comp);
+ }
+ // returnExpr
+ final HierarchyRuleParser.ReturnExprContext re =
+ (HierarchyRuleParser.ReturnExprContext) rv;
+ final HierarchyRuleModel.Expr value = new ExprVisitor().visitRuleExpr(re.ruleExpr());
+ return new HierarchyRuleModel.ReturnStatement(value);
+ }
+ }
+
+ /**
+ * Visitor for condition nodes.
+ */
+ private static final class ConditionVisitor
+ extends HierarchyRuleParserBaseVisitor<HierarchyRuleModel.Condition> {
+
+ @Override
+ public HierarchyRuleModel.Condition visitCondAnd(
+ final HierarchyRuleParser.CondAndContext ctx) {
+ return new HierarchyRuleModel.LogicalCondition(
+ visit(ctx.condition(0)),
+ HierarchyRuleModel.LogicalOp.AND,
+ visit(ctx.condition(1)));
+ }
+
+ @Override
+ public HierarchyRuleModel.Condition visitCondOr(
+ final HierarchyRuleParser.CondOrContext ctx) {
+ return new HierarchyRuleModel.LogicalCondition(
+ visit(ctx.condition(0)),
+ HierarchyRuleModel.LogicalOp.OR,
+ visit(ctx.condition(1)));
+ }
+
+ @Override
+ public HierarchyRuleModel.Condition visitCondNot(
+ final HierarchyRuleParser.CondNotContext ctx) {
+ return new HierarchyRuleModel.NotCondition(visit(ctx.condition()));
+ }
+
+ @Override
+ public HierarchyRuleModel.Condition visitCondParen(
+ final HierarchyRuleParser.CondParenContext ctx) {
+ return visit(ctx.condition());
+ }
+
+ @Override
+ public HierarchyRuleModel.Condition visitCondEq(
+ final HierarchyRuleParser.CondEqContext ctx) {
+ final ExprVisitor ev = new ExprVisitor();
+ return new HierarchyRuleModel.ComparisonCondition(
+ ev.visitRuleExpr(ctx.ruleExpr(0)),
+ HierarchyRuleModel.CompareOp.EQ,
+ ev.visitRuleExpr(ctx.ruleExpr(1)));
+ }
+
+ @Override
+ public HierarchyRuleModel.Condition visitCondNeq(
+ final HierarchyRuleParser.CondNeqContext ctx) {
+ final ExprVisitor ev = new ExprVisitor();
+ return new HierarchyRuleModel.ComparisonCondition(
+ ev.visitRuleExpr(ctx.ruleExpr(0)),
+ HierarchyRuleModel.CompareOp.NEQ,
+ ev.visitRuleExpr(ctx.ruleExpr(1)));
+ }
+
+ @Override
+ public HierarchyRuleModel.Condition visitCondGt(
+ final HierarchyRuleParser.CondGtContext ctx) {
+ final ExprVisitor ev = new ExprVisitor();
+ return new HierarchyRuleModel.ComparisonCondition(
+ ev.visitRuleExpr(ctx.ruleExpr(0)),
+ HierarchyRuleModel.CompareOp.GT,
+ ev.visitRuleExpr(ctx.ruleExpr(1)));
+ }
+
+ @Override
+ public HierarchyRuleModel.Condition visitCondLt(
+ final HierarchyRuleParser.CondLtContext ctx) {
+ final ExprVisitor ev = new ExprVisitor();
+ return new HierarchyRuleModel.ComparisonCondition(
+ ev.visitRuleExpr(ctx.ruleExpr(0)),
+ HierarchyRuleModel.CompareOp.LT,
+ ev.visitRuleExpr(ctx.ruleExpr(1)));
+ }
+
+ @Override
+ public HierarchyRuleModel.Condition visitCondExpr(
+ final HierarchyRuleParser.CondExprContext ctx) {
+ final ExprVisitor ev = new ExprVisitor();
+ return new HierarchyRuleModel.ExprCondition(ev.visitRuleExpr(ctx.ruleExpr()));
+ }
+ }
+
+ /**
+ * Visitor for expression nodes.
+ */
+ private static final class ExprVisitor
+ extends HierarchyRuleParserBaseVisitor<HierarchyRuleModel.Expr> {
+
+ public HierarchyRuleModel.Expr visitRuleExpr(
+ final HierarchyRuleParser.RuleExprContext ctx) {
+ return visit(ctx);
+ }
+
+ @Override
+ public HierarchyRuleModel.Expr visitExprAdd(
+ final HierarchyRuleParser.ExprAddContext ctx) {
+ return new HierarchyRuleModel.BinaryExpr(
+ visit(ctx.ruleExpr(0)),
+ HierarchyRuleModel.ArithmeticOp.ADD,
+ visit(ctx.ruleExpr(1)));
+ }
+
+ @Override
+ public HierarchyRuleModel.Expr visitExprSub(
+ final HierarchyRuleParser.ExprSubContext ctx) {
+ return new HierarchyRuleModel.BinaryExpr(
+ visit(ctx.ruleExpr(0)),
+ HierarchyRuleModel.ArithmeticOp.SUB,
+ visit(ctx.ruleExpr(1)));
+ }
+
+ @Override
+ public HierarchyRuleModel.Expr visitExprPrimary(
+ final HierarchyRuleParser.ExprPrimaryContext ctx) {
+ return visit(ctx.ruleExprPrimary());
+ }
+
+ @Override
+ public HierarchyRuleModel.Expr visitExprMethodChain(
+ final HierarchyRuleParser.ExprMethodChainContext ctx) {
+ return convertMethodChain(ctx.methodChain());
+ }
+
+ @Override
+ public HierarchyRuleModel.Expr visitExprString(
+ final HierarchyRuleParser.ExprStringContext ctx) {
+ return new HierarchyRuleModel.StringLiteralExpr(stripQuotes(ctx.STRING().getText()));
+ }
+
+ @Override
+ public HierarchyRuleModel.Expr visitExprNumber(
+ final HierarchyRuleParser.ExprNumberContext ctx) {
+ return new HierarchyRuleModel.NumberLiteralExpr(Long.parseLong(ctx.NUMBER().getText()));
+ }
+
+ @Override
+ public HierarchyRuleModel.Expr visitExprTrue(
+ final HierarchyRuleParser.ExprTrueContext ctx) {
+ return new HierarchyRuleModel.BoolLiteralExpr(true);
+ }
+
+ @Override
+ public HierarchyRuleModel.Expr visitExprFalse(
+ final HierarchyRuleParser.ExprFalseContext ctx) {
+ return new HierarchyRuleModel.BoolLiteralExpr(false);
+ }
+
+ private HierarchyRuleModel.MethodChainExpr convertMethodChain(
+ final HierarchyRuleParser.MethodChainContext ctx) {
+ final String target = ctx.IDENTIFIER().getText();
+ final List<HierarchyRuleModel.ChainSegment> segments = new ArrayList<>();
+ for (final HierarchyRuleParser.ChainSegmentContext seg : ctx.chainSegment()) {
+ segments.add(convertChainSegment(seg));
+ }
+ return new HierarchyRuleModel.MethodChainExpr(target, segments);
+ }
+
+ private HierarchyRuleModel.ChainSegment convertChainSegment(
+ final HierarchyRuleParser.ChainSegmentContext ctx) {
+ if (ctx instanceof HierarchyRuleParser.ChainMethodCallContext) {
+ final HierarchyRuleParser.ChainMethodCallContext mc =
+ (HierarchyRuleParser.ChainMethodCallContext) ctx;
+ final String name = mc.IDENTIFIER().getText();
+ final List<HierarchyRuleModel.Expr> args = new ArrayList<>();
+ if (mc.argList() != null) {
+ for (final HierarchyRuleParser.RuleExprContext argCtx :
+ mc.argList().ruleExpr()) {
+ args.add(visit(argCtx));
+ }
+ }
+ return new HierarchyRuleModel.MethodCallSegment(name, args);
+ }
+ final HierarchyRuleParser.ChainFieldAccessContext fa =
+ (HierarchyRuleParser.ChainFieldAccessContext) ctx;
+ return new HierarchyRuleModel.FieldAccess(fa.IDENTIFIER().getText());
+ }
+ }
+
+ private static String stripQuotes(final String s) {
+ if (s.length() >= 2 && (s.charAt(0) == '\'' || s.charAt(0) == '"')) {
+ return s.substring(1, s.length() - 1);
+ }
+ return s;
+ }
+}
diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/MalExpression.java b/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/compiler/rt/HierarchyRulePackageHolder.java
similarity index 72%
copy from oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/MalExpression.java
copy to oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/compiler/rt/HierarchyRulePackageHolder.java
index cfaf1d5bee..31d74d245d 100644
--- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/MalExpression.java
+++ b/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/compiler/rt/HierarchyRulePackageHolder.java
@@ -13,18 +13,14 @@
* 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.util.Map;
+package org.apache.skywalking.oap.server.core.config.compiler.rt;
/**
- * Pure Java replacement for Groovy-based MAL DelegatingScript.
- * Each transpiled MAL expression implements this interface.
+ * Empty marker class used as the class loading anchor for Javassist
+ * {@code CtClass.toClass(Class)} on JDK 16+.
+ * Generated hierarchy rule classes are loaded in this package.
*/
-@FunctionalInterface
-public interface MalExpression {
- SampleFamily run(Map<String, SampleFamily> samples);
+public class HierarchyRulePackageHolder {
}
diff --git a/oap-server/analyzer/hierarchy/src/test/java/org/apache/skywalking/oap/server/core/config/compiler/HierarchyRuleClassGeneratorTest.java b/oap-server/analyzer/hierarchy/src/test/java/org/apache/skywalking/oap/server/core/config/compiler/HierarchyRuleClassGeneratorTest.java
new file mode 100644
index 0000000000..1054bcb27b
--- /dev/null
+++ b/oap-server/analyzer/hierarchy/src/test/java/org/apache/skywalking/oap/server/core/config/compiler/HierarchyRuleClassGeneratorTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.compiler;
+
+import java.util.function.BiFunction;
+import javassist.ClassPool;
+import org.apache.skywalking.oap.server.core.query.type.Service;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class HierarchyRuleClassGeneratorTest {
+
+ private HierarchyRuleClassGenerator generator;
+
+ @BeforeEach
+ void setUp() {
+ generator = new HierarchyRuleClassGenerator(new ClassPool(true));
+ }
+
+ @Test
+ void compileSimpleNameEquality() throws Exception {
+ final BiFunction<Service, Service, Boolean> fn = generator.compile(
+ "name", "{ (u, l) -> u.name == l.name }");
+
+ assertNotNull(fn);
+
+ final Service upper = new Service();
+ upper.setName("svc-a");
+ final Service lower = new Service();
+ lower.setName("svc-a");
+ assertTrue(fn.apply(upper, lower));
+
+ lower.setName("svc-b");
+ assertFalse(fn.apply(upper, lower));
+ }
+
+ @Test
+ void compileShortNameEquality() throws Exception {
+ final BiFunction<Service, Service, Boolean> fn = generator.compile(
+ "short-name", "{ (u, l) -> u.shortName == l.shortName }");
+
+ assertNotNull(fn);
+
+ final Service upper = new Service();
+ upper.setShortName("svc");
+ final Service lower = new Service();
+ lower.setShortName("svc");
+ assertTrue(fn.apply(upper, lower));
+
+ lower.setShortName("other");
+ assertFalse(fn.apply(upper, lower));
+ }
+
+ @Test
+ void compileLowerShortNameRemoveNs() throws Exception {
+ final String expr = "{ (u, l) -> {"
+ + " if (l.shortName.lastIndexOf('.') > 0) {"
+ + " return u.shortName == l.shortName.substring(0, l.shortName.lastIndexOf('.'));"
+ + " }"
+ + " return false;"
+ + "} }";
+ final BiFunction<Service, Service, Boolean> fn = generator.compile(
+ "lower-short-name-remove-ns", expr);
+
+ assertNotNull(fn);
+
+ final Service upper = new Service();
+ upper.setShortName("svc-a");
+ final Service lower = new Service();
+ lower.setShortName("svc-a.ns1");
+ assertTrue(fn.apply(upper, lower));
+
+ lower.setShortName("svc-b.ns1");
+ assertFalse(fn.apply(upper, lower));
+
+ lower.setShortName("no-dot");
+ assertFalse(fn.apply(upper, lower));
+ }
+
+ @Test
+ void compileLowerShortNameWithFqdn() throws Exception {
+ final String expr = "{ (u, l) -> {"
+ + " if (u.shortName.lastIndexOf(':') > 0) {"
+ + " return u.shortName.substring(0, u.shortName.lastIndexOf(':'))"
+ + " == l.shortName.concat('.svc.cluster.local');"
+ + " }"
+ + " return false;"
+ + "} }";
+ final BiFunction<Service, Service, Boolean> fn = generator.compile(
+ "lower-short-name-with-fqdn", expr);
+
+ assertNotNull(fn);
+
+ final Service upper = new Service();
+ upper.setShortName("svc-a.svc.cluster.local:8080");
+ final Service lower = new Service();
+ lower.setShortName("svc-a");
+ assertTrue(fn.apply(upper, lower));
+
+ upper.setShortName("no-port");
+ assertFalse(fn.apply(upper, lower));
+ }
+}
diff --git a/oap-server/analyzer/hierarchy/src/test/java/org/apache/skywalking/oap/server/core/config/compiler/HierarchyRuleScriptParserTest.java b/oap-server/analyzer/hierarchy/src/test/java/org/apache/skywalking/oap/server/core/config/compiler/HierarchyRuleScriptParserTest.java
new file mode 100644
index 0000000000..1cce3c3271
--- /dev/null
+++ b/oap-server/analyzer/hierarchy/src/test/java/org/apache/skywalking/oap/server/core/config/compiler/HierarchyRuleScriptParserTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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.compiler;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class HierarchyRuleScriptParserTest {
+
+ @Test
+ void parseSimpleNameEquality() {
+ final HierarchyRuleModel model = HierarchyRuleScriptParser.parse(
+ "{ (u, l) -> u.name == l.name }");
+
+ assertEquals("u", model.getUpperParam());
+ assertEquals("l", model.getLowerParam());
+ assertInstanceOf(HierarchyRuleModel.SimpleComparison.class, model.getBody());
+
+ final HierarchyRuleModel.SimpleComparison cmp =
+ (HierarchyRuleModel.SimpleComparison) model.getBody();
+ assertEquals(HierarchyRuleModel.CompareOp.EQ, cmp.getOp());
+
+ final HierarchyRuleModel.MethodChainExpr left =
+ (HierarchyRuleModel.MethodChainExpr) cmp.getLeft();
+ assertEquals("u", left.getTarget());
+ assertEquals(1, left.getSegments().size());
+ assertEquals("name", left.getSegments().get(0).getName());
+
+ final HierarchyRuleModel.MethodChainExpr right =
+ (HierarchyRuleModel.MethodChainExpr) cmp.getRight();
+ assertEquals("l", right.getTarget());
+ assertEquals("name", right.getSegments().get(0).getName());
+ }
+
+ @Test
+ void parseShortNameEquality() {
+ final HierarchyRuleModel model = HierarchyRuleScriptParser.parse(
+ "{ (u, l) -> u.shortName == l.shortName }");
+
+ final HierarchyRuleModel.SimpleComparison cmp =
+ (HierarchyRuleModel.SimpleComparison) model.getBody();
+ final HierarchyRuleModel.MethodChainExpr left =
+ (HierarchyRuleModel.MethodChainExpr) cmp.getLeft();
+ assertEquals("shortName", left.getSegments().get(0).getName());
+ }
+
+ @Test
+ void parseLowerShortNameRemoveNs() {
+ // lower-short-name-remove-ns rule
+ final String expr =
+ "{ (u, l) -> { if(l.shortName.lastIndexOf('.') > 0) "
+ + "return u.shortName == l.shortName.substring(0, l.shortName.lastIndexOf('.')); "
+ + "return false; } }";
+
+ final HierarchyRuleModel model = HierarchyRuleScriptParser.parse(expr);
+ assertEquals("u", model.getUpperParam());
+ assertEquals("l", model.getLowerParam());
+ assertInstanceOf(HierarchyRuleModel.BlockBody.class, model.getBody());
+
+ final HierarchyRuleModel.BlockBody block =
+ (HierarchyRuleModel.BlockBody) model.getBody();
+ assertEquals(2, block.getStatements().size());
+
+ // First statement: if
+ final HierarchyRuleModel.IfStatement ifStmt =
+ (HierarchyRuleModel.IfStatement) block.getStatements().get(0);
+ assertInstanceOf(
+ HierarchyRuleModel.ComparisonCondition.class, ifStmt.getCondition());
+ final HierarchyRuleModel.ComparisonCondition cond =
+ (HierarchyRuleModel.ComparisonCondition) ifStmt.getCondition();
+ assertEquals(HierarchyRuleModel.CompareOp.GT, cond.getOp());
+
+ // Condition left: l.shortName.lastIndexOf('.')
+ final HierarchyRuleModel.MethodChainExpr condLeft =
+ (HierarchyRuleModel.MethodChainExpr) cond.getLeft();
+ assertEquals("l", condLeft.getTarget());
+ assertEquals(2, condLeft.getSegments().size());
+ assertEquals("shortName", condLeft.getSegments().get(0).getName());
+ assertInstanceOf(
+ HierarchyRuleModel.MethodCallSegment.class, condLeft.getSegments().get(1));
+ final HierarchyRuleModel.MethodCallSegment lastIndexOf =
+ (HierarchyRuleModel.MethodCallSegment) condLeft.getSegments().get(1);
+ assertEquals("lastIndexOf", lastIndexOf.getName());
+ assertEquals(1, lastIndexOf.getArguments().size());
+ assertInstanceOf(
+ HierarchyRuleModel.StringLiteralExpr.class, lastIndexOf.getArguments().get(0));
+ assertEquals(".",
+ ((HierarchyRuleModel.StringLiteralExpr) lastIndexOf.getArguments().get(0)).getValue());
+
+ // Then branch: return u.shortName == l.shortName.substring(0, ...)
+ assertEquals(1, ifStmt.getThenBranch().size());
+ assertInstanceOf(
+ HierarchyRuleModel.ReturnStatement.class, ifStmt.getThenBranch().get(0));
+
+ // Second statement: return false
+ final HierarchyRuleModel.ReturnStatement retFalse =
+ (HierarchyRuleModel.ReturnStatement) block.getStatements().get(1);
+ assertInstanceOf(HierarchyRuleModel.BoolLiteralExpr.class, retFalse.getValue());
+ final HierarchyRuleModel.BoolLiteralExpr falseExpr =
+ (HierarchyRuleModel.BoolLiteralExpr) retFalse.getValue();
+ assertTrue(!falseExpr.isValue());
+ }
+
+ @Test
+ void parseLowerShortNameWithFqdn() {
+ // lower-short-name-with-fqdn rule
+ final String expr =
+ "{ (u, l) -> { if(u.shortName.lastIndexOf(':') > 0) "
+ + "return u.shortName.substring(0, u.shortName.lastIndexOf(':')) "
+ + "== l.shortName.concat('.svc.cluster.local'); "
+ + "return false; } }";
+
+ final HierarchyRuleModel model = HierarchyRuleScriptParser.parse(expr);
+ assertInstanceOf(HierarchyRuleModel.BlockBody.class, model.getBody());
+
+ final HierarchyRuleModel.BlockBody block =
+ (HierarchyRuleModel.BlockBody) model.getBody();
+ assertEquals(2, block.getStatements().size());
+
+ // Verify the if condition checks u.shortName.lastIndexOf(':') > 0
+ final HierarchyRuleModel.IfStatement ifStmt =
+ (HierarchyRuleModel.IfStatement) block.getStatements().get(0);
+ final HierarchyRuleModel.ComparisonCondition cond =
+ (HierarchyRuleModel.ComparisonCondition) ifStmt.getCondition();
+ assertEquals(HierarchyRuleModel.CompareOp.GT, cond.getOp());
+
+ // Then branch has a return statement with == comparison
+ final HierarchyRuleModel.ReturnStatement retStmt =
+ (HierarchyRuleModel.ReturnStatement) ifStmt.getThenBranch().get(0);
+ // The return value should be a comparison (u.shortName.substring(...) == l.shortName.concat(...))
+ // But since our grammar wraps returns as expressions, check the structure
+ assertInstanceOf(HierarchyRuleModel.Expr.class, retStmt.getValue());
+ }
+
+ @Test
+ void parseSyntaxErrorThrows() {
+ assertThrows(IllegalArgumentException.class,
+ () -> HierarchyRuleScriptParser.parse("{ invalid }"));
+ }
+}
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
deleted file mode 100644
index 5df94e71db..0000000000
--- a/oap-server/analyzer/lal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/lal/LalToJavaTranspiler.java
+++ /dev/null
@@ -1,923 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.skywalking.oap.server.transpiler.lal;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.StringWriter;
-import java.nio.file.Files;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-import javax.tools.JavaCompiler;
-import javax.tools.JavaFileObject;
-import javax.tools.StandardJavaFileManager;
-import javax.tools.ToolProvider;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.skywalking.oap.server.transpiler.mal.MalToJavaTranspiler;
-import org.codehaus.groovy.ast.ModuleNode;
-import org.codehaus.groovy.ast.expr.ArgumentListExpression;
-import org.codehaus.groovy.ast.expr.BinaryExpression;
-import org.codehaus.groovy.ast.expr.BooleanExpression;
-import org.codehaus.groovy.ast.expr.CastExpression;
-import org.codehaus.groovy.ast.expr.ClosureExpression;
-import org.codehaus.groovy.ast.expr.ConstantExpression;
-import org.codehaus.groovy.ast.expr.Expression;
-import org.codehaus.groovy.ast.expr.GStringExpression;
-import org.codehaus.groovy.ast.expr.MapEntryExpression;
-import org.codehaus.groovy.ast.expr.MapExpression;
-import org.codehaus.groovy.ast.expr.MethodCallExpression;
-import org.codehaus.groovy.ast.expr.NotExpression;
-import org.codehaus.groovy.ast.expr.PropertyExpression;
-import org.codehaus.groovy.ast.expr.TupleExpression;
-import org.codehaus.groovy.ast.expr.VariableExpression;
-import org.codehaus.groovy.ast.stmt.BlockStatement;
-import org.codehaus.groovy.ast.stmt.EmptyStatement;
-import org.codehaus.groovy.ast.stmt.ExpressionStatement;
-import org.codehaus.groovy.ast.stmt.IfStatement;
-import org.codehaus.groovy.ast.stmt.Statement;
-import org.codehaus.groovy.control.CompilationUnit;
-import org.codehaus.groovy.control.CompilerConfiguration;
-import org.codehaus.groovy.control.Phases;
-import org.codehaus.groovy.syntax.Types;
-
-/**
- * Transpiles LAL (Log Analysis Language) Groovy scripts to Java source code at build time.
- * Parses DSL strings into Groovy AST (via CompilationUnit at CONVERSION phase),
- * walks AST nodes, and produces Java classes implementing LalExpression.
- */
-@Slf4j
-public class LalToJavaTranspiler {
-
- 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(
- "text", "extractor", "sink", "slowSql", "sampledTrace", "metrics", "sampler"
- );
-
- private static final Set<String> STATIC_TYPES = Set.of("ProcessRegistry");
-
- // ---- Batch state ----
-
- private final Map<String, String> lalSources = new LinkedHashMap<>();
- private final Map<String, String> hashToClass = new LinkedHashMap<>();
-
- private int tempVarCounter;
-
- /**
- * Transpile a LAL DSL script to a Java class implementing LalExpression.
- *
- * @param className simple class name (e.g. "LalExpr_3")
- * @param dslText the Groovy DSL string (filter { ... })
- * @return Java source code
- */
- public String transpile(final String className, final String dslText) {
- tempVarCounter = 0;
- final ModuleNode module = parseToAST(dslText);
- final Statement body = extractBody(module);
-
- final StringBuilder sb = new StringBuilder();
- emitHeader(sb, className);
-
- final List<Statement> stmts = flattenStatements(body);
- for (Statement stmt : stmts) {
- emitStatement(sb, stmt, "filterSpec", "binding", 2);
- }
-
- emitFooter(sb);
- return sb.toString();
- }
-
- public void register(final String className, final String hash, final String source) {
- lalSources.put(className, source);
- hashToClass.put(hash, GENERATED_PACKAGE + "." + className);
- }
-
- // ---- AST Parsing ----
-
- ModuleNode parseToAST(final String expression) {
- final CompilerConfiguration cc = new CompilerConfiguration();
- final CompilationUnit cu = new CompilationUnit(cc);
- cu.addSource("Script", expression);
- cu.compile(Phases.CONVERSION);
- final List<ModuleNode> modules = cu.getAST().getModules();
- if (modules.isEmpty()) {
- throw new IllegalStateException("No AST modules produced");
- }
- return modules.get(0);
- }
-
- Statement extractBody(final ModuleNode module) {
- final BlockStatement block = module.getStatementBlock();
- if (block != null && !block.getStatements().isEmpty()) {
- return block;
- }
- throw new IllegalStateException("Empty AST body");
- }
-
- // ---- Code Generation ----
-
- private void emitHeader(final StringBuilder sb, final String className) {
- sb.append("package ").append(GENERATED_PACKAGE).append(";\n\n");
- sb.append("import java.util.Map;\n");
- sb.append("import org.apache.skywalking.oap.log.analyzer.dsl.Binding;\n");
- sb.append("import org.apache.skywalking.oap.log.analyzer.dsl.LalExpression;\n");
- sb.append("import org.apache.skywalking.oap.log.analyzer.dsl.spec.filter.FilterSpec;\n\n");
- sb.append("@SuppressWarnings(\"unchecked\")\n");
- sb.append("public class ").append(className).append(" implements LalExpression {\n\n");
- emitHelpers(sb);
- sb.append(" @Override\n");
- sb.append(" public void execute(FilterSpec filterSpec, Binding binding) {\n");
- }
-
- private void emitHelpers(final StringBuilder sb) {
- sb.append(" private static Object getAt(Object obj, String key) {\n");
- sb.append(" if (obj == null) return null;\n");
- sb.append(" if (obj instanceof Binding.Parsed) return ((Binding.Parsed) obj).getAt(key);\n");
- sb.append(" if (obj instanceof Map) return ((Map<String, Object>) obj).get(key);\n");
- sb.append(" return null;\n");
- sb.append(" }\n\n");
-
- sb.append(" private static long toLong(Object obj) {\n");
- sb.append(" if (obj instanceof Number) return ((Number) obj).longValue();\n");
- sb.append(" if (obj instanceof String) return Long.parseLong((String) obj);\n");
- sb.append(" return 0L;\n");
- sb.append(" }\n\n");
-
- sb.append(" private static int toInt(Object obj) {\n");
- sb.append(" if (obj instanceof Number) return ((Number) obj).intValue();\n");
- sb.append(" if (obj instanceof String) return Integer.parseInt((String) obj);\n");
- sb.append(" return 0;\n");
- sb.append(" }\n\n");
-
- sb.append(" private static boolean toBoolean(Object obj) {\n");
- sb.append(" if (obj instanceof Boolean) return (Boolean) obj;\n");
- sb.append(" if (obj instanceof String) return Boolean.parseBoolean((String) obj);\n");
- sb.append(" return obj != null;\n");
- sb.append(" }\n\n");
-
- sb.append(" private static boolean isTruthy(Object obj) {\n");
- sb.append(" if (obj == null) return false;\n");
- sb.append(" if (obj instanceof Boolean) return (Boolean) obj;\n");
- sb.append(" if (obj instanceof String) return !((String) obj).isEmpty();\n");
- sb.append(" if (obj instanceof Number) return ((Number) obj).doubleValue() != 0;\n");
- sb.append(" return true;\n");
- sb.append(" }\n\n");
-
- sb.append(" private static boolean isNonEmptyString(Object obj) {\n");
- sb.append(" if (obj == null) return false;\n");
- sb.append(" String s = obj.toString();\n");
- sb.append(" return s != null && !s.trim().isEmpty();\n");
- sb.append(" }\n\n");
- }
-
- private void emitFooter(final StringBuilder sb) {
- sb.append(" }\n");
- sb.append("}\n");
- }
-
- private void emitStatement(final StringBuilder sb, final Statement stmt,
- final String receiver, final String bindingVar, final int indent) {
- if (stmt instanceof BlockStatement) {
- for (Statement s : ((BlockStatement) stmt).getStatements()) {
- emitStatement(sb, s, receiver, bindingVar, indent);
- }
- } else if (stmt instanceof ExpressionStatement) {
- emitExpressionStatement(sb, ((ExpressionStatement) stmt).getExpression(),
- receiver, bindingVar, indent);
- } else if (stmt instanceof IfStatement) {
- emitIfStatement(sb, (IfStatement) stmt, receiver, bindingVar, indent);
- } else if (stmt instanceof EmptyStatement) {
- // skip
- } else {
- throw new UnsupportedOperationException(
- "Unsupported statement: " + stmt.getClass().getSimpleName());
- }
- }
-
- private void emitExpressionStatement(final StringBuilder sb, final Expression expr,
- final String receiver, final String bindingVar,
- final int indent) {
- if (expr instanceof MethodCallExpression) {
- emitMethodCall(sb, (MethodCallExpression) expr, receiver, bindingVar, indent);
- } else if (expr instanceof BinaryExpression) {
- emitBinaryAsNamedArg(sb, (BinaryExpression) expr, receiver, bindingVar, indent);
- } else {
- throw new UnsupportedOperationException(
- "Unsupported expression statement: " + expr.getClass().getSimpleName()
- + " (" + expr.getText() + ")");
- }
- }
-
- private void emitMethodCall(final StringBuilder sb, final MethodCallExpression mce,
- final String receiver, final String bindingVar, final int indent) {
- final String methodName = mce.getMethodAsString();
- final Expression objExpr = mce.getObjectExpression();
- final ArgumentListExpression args = toArgList(mce.getArguments());
- final List<Expression> argExprs = args.getExpressions();
-
- // Top-level filter { ... } -> unwrap and emit body
- if ("filter".equals(methodName) && isThisOrImplicit(objExpr)) {
- if (!argExprs.isEmpty() && argExprs.get(0) instanceof ClosureExpression) {
- final ClosureExpression closure = (ClosureExpression) argExprs.get(0);
- emitStatement(sb, closure.getCode(), receiver, bindingVar, indent);
- return;
- }
- }
-
- // json {} -> filterSpec.json()
- if ("json".equals(methodName) && isThisOrImplicit(objExpr)) {
- indent(sb, indent);
- sb.append(receiver).append(".json();\n");
- return;
- }
-
- // text { regexp /pattern/ } -> filterSpec.text(tp -> tp.regexp("pattern"))
- if ("text".equals(methodName) && isThisOrImplicit(objExpr)
- && !argExprs.isEmpty() && argExprs.get(0) instanceof ClosureExpression) {
- final ClosureExpression closure = (ClosureExpression) argExprs.get(0);
- final String tpVar = "tp";
- indent(sb, indent);
- sb.append(receiver).append(".text(").append(tpVar).append(" -> {\n");
- emitStatement(sb, closure.getCode(), tpVar, bindingVar, indent + 1);
- indent(sb, indent);
- sb.append("});\n");
- return;
- }
-
- // regexp /pattern/ -> tp.regexp("pattern")
- if ("regexp".equals(methodName) && isThisOrImplicit(objExpr)) {
- indent(sb, indent);
- sb.append(receiver).append(".regexp(");
- if (!argExprs.isEmpty()) {
- sb.append(visitValueExpression(argExprs.get(0), bindingVar));
- }
- sb.append(");\n");
- return;
- }
-
- // Consumer overload methods: extractor, sink, slowSql, sampledTrace, metrics, sampler
- if (CONSUMER_METHODS.contains(methodName) && isThisOrImplicit(objExpr)
- && !argExprs.isEmpty() && argExprs.get(0) instanceof ClosureExpression) {
- final ClosureExpression closure = (ClosureExpression) argExprs.get(0);
- final String lambdaVar = lambdaVarFor(methodName);
- final String childReceiver = childReceiverFor(lambdaVar);
- indent(sb, indent);
- sb.append(receiver).append(".").append(methodName).append("(")
- .append(lambdaVar).append(" -> {\n");
- emitStatement(sb, closure.getCode(), childReceiver, bindingVar, indent + 1);
- indent(sb, indent);
- sb.append("});\n");
- return;
- }
-
- // rateLimit("${expr}") { rpm N } -> sp.rateLimit(idExpr, rls -> rls.rpm(N))
- if ("rateLimit".equals(methodName) && isThisOrImplicit(objExpr)) {
- final Expression idArg = argExprs.get(0);
- final ClosureExpression closure = argExprs.size() > 1
- && argExprs.get(1) instanceof ClosureExpression
- ? (ClosureExpression) argExprs.get(1) : null;
-
- indent(sb, indent);
- sb.append(receiver).append(".rateLimit(")
- .append(visitValueExpression(idArg, bindingVar));
- if (closure != null) {
- final String rlsVar = "rls";
- sb.append(", ").append(rlsVar).append(" -> {\n");
- emitStatement(sb, closure.getCode(), rlsVar, bindingVar, indent + 1);
- indent(sb, indent);
- sb.append("}");
- }
- sb.append(");\n");
- return;
- }
-
- // abort {} -> filterSpec.abort()
- if ("abort".equals(methodName) && isThisOrImplicit(objExpr)) {
- indent(sb, indent);
- sb.append(receiver).append(".abort();\n");
- return;
- }
-
- // enforcer {} or dropper {} -> sink.enforcer() / sink.dropper()
- if (("enforcer".equals(methodName) || "dropper".equals(methodName))
- && isThisOrImplicit(objExpr)) {
- indent(sb, indent);
- sb.append(receiver).append(".").append(methodName).append("();\n");
- return;
- }
-
- // tag("KEY") as standalone statement
- if ("tag".equals(methodName) && isThisOrImplicit(objExpr)
- && !argExprs.isEmpty() && argExprs.get(0) instanceof ConstantExpression) {
- indent(sb, indent);
- sb.append(receiver).append(".tag(\"")
- .append(MalToJavaTranspiler.escapeJava(argExprs.get(0).getText()))
- .append("\");\n");
- return;
- }
-
- // Simple value-setting methods: service(val), layer(val), timestamp(val), etc.
- if (isThisOrImplicit(objExpr)) {
- indent(sb, indent);
- sb.append(receiver).append(".").append(methodName).append("(");
- for (int i = 0; i < argExprs.size(); i++) {
- if (i > 0) {
- sb.append(", ");
- }
- sb.append(visitValueExpression(argExprs.get(i), bindingVar));
- }
- sb.append(");\n");
- return;
- }
-
- // Static method: ProcessRegistry.generateVirtualLocalProcess(...)
- if (objExpr instanceof VariableExpression
- && STATIC_TYPES.contains(((VariableExpression) objExpr).getName())) {
- indent(sb, indent);
- sb.append("org.apache.skywalking.oap.meter.analyzer.dsl.registry.ProcessRegistry.")
- .append(methodName).append("(");
- for (int i = 0; i < argExprs.size(); i++) {
- if (i > 0) {
- sb.append(", ");
- }
- sb.append(visitValueExpression(argExprs.get(i), bindingVar));
- }
- sb.append(");\n");
- return;
- }
-
- throw new UnsupportedOperationException(
- "Unsupported method call: " + methodName + " on " + objExpr.getClass().getSimpleName()
- + " (" + mce.getText() + ")");
- }
-
- private void emitBinaryAsNamedArg(final StringBuilder sb, final BinaryExpression expr,
- final String receiver, final String bindingVar,
- final int indent) {
- throw new UnsupportedOperationException(
- "Unsupported binary expression as statement: " + expr.getText());
- }
-
- // ---- If/Else ----
-
- private void emitIfStatement(final StringBuilder sb, final IfStatement ifStmt,
- final String receiver, final String bindingVar, final int indent) {
- indent(sb, indent);
- sb.append("if (");
- sb.append(visitCondition(ifStmt.getBooleanExpression(), bindingVar));
- sb.append(") {\n");
-
- emitStatement(sb, ifStmt.getIfBlock(), receiver, bindingVar, indent + 1);
-
- final Statement elseBlock = ifStmt.getElseBlock();
- if (elseBlock != null && !(elseBlock instanceof EmptyStatement)) {
- indent(sb, indent);
- if (elseBlock instanceof IfStatement) {
- sb.append("} else ");
- emitIfStatementInline(sb, (IfStatement) elseBlock, receiver, bindingVar, indent);
- return;
- } else {
- sb.append("} else {\n");
- emitStatement(sb, elseBlock, receiver, bindingVar, indent + 1);
- indent(sb, indent);
- sb.append("}\n");
- }
- } else {
- indent(sb, indent);
- sb.append("}\n");
- }
- }
-
- private void emitIfStatementInline(final StringBuilder sb, final IfStatement ifStmt,
- final String receiver, final String bindingVar,
- final int indent) {
- sb.append("if (");
- sb.append(visitCondition(ifStmt.getBooleanExpression(), bindingVar));
- sb.append(") {\n");
-
- emitStatement(sb, ifStmt.getIfBlock(), receiver, bindingVar, indent + 1);
-
- final Statement elseBlock = ifStmt.getElseBlock();
- if (elseBlock != null && !(elseBlock instanceof EmptyStatement)) {
- indent(sb, indent);
- if (elseBlock instanceof IfStatement) {
- sb.append("} else ");
- emitIfStatementInline(sb, (IfStatement) elseBlock, receiver, bindingVar, indent);
- } else {
- sb.append("} else {\n");
- emitStatement(sb, elseBlock, receiver, bindingVar, indent + 1);
- indent(sb, indent);
- sb.append("}\n");
- }
- } else {
- indent(sb, indent);
- sb.append("}\n");
- }
- }
-
- // ---- Condition Visiting ----
-
- private String visitCondition(final BooleanExpression boolExpr, final String bindingVar) {
- return visitConditionExpr(boolExpr.getExpression(), bindingVar);
- }
-
- String visitConditionExpr(final Expression expr, final String bindingVar) {
- if (expr instanceof BinaryExpression) {
- final BinaryExpression bin = (BinaryExpression) expr;
- final int op = bin.getOperation().getType();
-
- if (op == Types.COMPARE_EQUAL) {
- final String left = visitValueExpression(bin.getLeftExpression(), bindingVar);
- final String right = visitValueExpression(bin.getRightExpression(), bindingVar);
- if (bin.getRightExpression() instanceof ConstantExpression
- && ((ConstantExpression) bin.getRightExpression()).getValue() instanceof String) {
- return "\"" + MalToJavaTranspiler.escapeJava(
- (String) ((ConstantExpression) bin.getRightExpression()).getValue())
- + "\".equals(" + left + ")";
- }
- return "java.util.Objects.equals(" + left + ", " + right + ")";
- }
-
- if (op == Types.COMPARE_NOT_EQUAL) {
- final String left = visitValueExpression(bin.getLeftExpression(), bindingVar);
- final String right = visitValueExpression(bin.getRightExpression(), bindingVar);
- if (bin.getRightExpression() instanceof ConstantExpression
- && ((ConstantExpression) bin.getRightExpression()).getValue() instanceof String) {
- return "!\"" + MalToJavaTranspiler.escapeJava(
- (String) ((ConstantExpression) bin.getRightExpression()).getValue())
- + "\".equals(" + left + ")";
- }
- return "!java.util.Objects.equals(" + left + ", " + right + ")";
- }
-
- if (op == Types.COMPARE_LESS_THAN) {
- final String left = visitValueExpression(bin.getLeftExpression(), bindingVar);
- final String right = visitValueExpression(bin.getRightExpression(), bindingVar);
- return "toInt(" + left + ") < " + right;
- }
-
- if (op == Types.COMPARE_GREATER_THAN_EQUAL) {
- final String left = visitValueExpression(bin.getLeftExpression(), bindingVar);
- final String right = visitValueExpression(bin.getRightExpression(), bindingVar);
- return "toInt(" + left + ") >= " + right;
- }
-
- if (op == Types.LOGICAL_AND) {
- return visitConditionExpr(bin.getLeftExpression(), bindingVar)
- + " && " + visitConditionExpr(bin.getRightExpression(), bindingVar);
- }
-
- if (op == Types.LOGICAL_OR) {
- return visitConditionExpr(bin.getLeftExpression(), bindingVar)
- + " || " + visitConditionExpr(bin.getRightExpression(), bindingVar);
- }
-
- throw new UnsupportedOperationException(
- "Unsupported condition operator: " + bin.getOperation().getText());
- }
-
- if (expr instanceof NotExpression) {
- final String inner = visitConditionExpr(((NotExpression) expr).getExpression(), bindingVar);
- return "!" + inner;
- }
-
- if (expr instanceof BooleanExpression) {
- return visitConditionExpr(((BooleanExpression) expr).getExpression(), bindingVar);
- }
-
- if (expr instanceof MethodCallExpression) {
- final MethodCallExpression mce = (MethodCallExpression) expr;
- final String methodName = mce.getMethodAsString();
- if ("toString".equals(methodName) || "trim".equals(methodName)) {
- final String obj = visitValueExpression(mce.getObjectExpression(), bindingVar);
- return "isNonEmptyString(" + obj + ")";
- }
- return visitValueExpression(expr, bindingVar);
- }
-
- return "isTruthy(" + visitValueExpression(expr, bindingVar) + ")";
- }
-
- // ---- Value Expression Visiting ----
-
- String visitValueExpression(final Expression expr, final String bindingVar) {
- if (expr instanceof ConstantExpression) {
- return visitConstant((ConstantExpression) expr);
- }
-
- if (expr instanceof VariableExpression) {
- return visitVariable((VariableExpression) expr, bindingVar);
- }
-
- if (expr instanceof PropertyExpression) {
- return visitProperty((PropertyExpression) expr, bindingVar);
- }
-
- if (expr instanceof CastExpression) {
- return visitCast((CastExpression) expr, bindingVar);
- }
-
- if (expr instanceof MethodCallExpression) {
- return visitMethodCallValue((MethodCallExpression) expr, bindingVar);
- }
-
- if (expr instanceof GStringExpression) {
- return visitGString((GStringExpression) expr, bindingVar);
- }
-
- if (expr instanceof MapExpression) {
- return visitMapExpression((MapExpression) expr, bindingVar);
- }
-
- if (expr instanceof BinaryExpression) {
- final BinaryExpression bin = (BinaryExpression) expr;
- final int op = bin.getOperation().getType();
- if (op == Types.COMPARE_EQUAL || op == Types.COMPARE_NOT_EQUAL
- || op == Types.LOGICAL_AND || op == Types.LOGICAL_OR
- || op == Types.COMPARE_LESS_THAN || op == Types.COMPARE_GREATER_THAN_EQUAL) {
- return visitConditionExpr(expr, bindingVar);
- }
- }
-
- if (expr instanceof NotExpression) {
- return "!" + visitValueExpression(((NotExpression) expr).getExpression(), bindingVar);
- }
-
- throw new UnsupportedOperationException(
- "Unsupported value expression: " + expr.getClass().getSimpleName()
- + " (" + expr.getText() + ")");
- }
-
- private String visitConstant(final ConstantExpression expr) {
- final Object value = expr.getValue();
- if (value instanceof String) {
- return "\"" + MalToJavaTranspiler.escapeJava((String) value) + "\"";
- }
- if (value instanceof Integer) {
- return value.toString();
- }
- if (value instanceof Long) {
- return value + "L";
- }
- if (value instanceof Boolean) {
- return value.toString();
- }
- if (value instanceof Double) {
- return value + "d";
- }
- if (value == null) {
- return "null";
- }
- return value.toString();
- }
-
- private String visitVariable(final VariableExpression expr, final String bindingVar) {
- final String name = expr.getName();
- if ("parsed".equals(name)) {
- return bindingVar + ".parsed()";
- }
- if ("log".equals(name)) {
- return bindingVar + ".log()";
- }
- if ("this".equals(name)) {
- return "filterSpec";
- }
- if (STATIC_TYPES.contains(name)) {
- return "org.apache.skywalking.oap.meter.analyzer.dsl.registry.ProcessRegistry";
- }
- return name;
- }
-
- private String visitProperty(final PropertyExpression expr, final String bindingVar) {
- final Expression objExpr = expr.getObjectExpression();
- final String propName = expr.getPropertyAsString();
- final boolean isSafe = expr.isSafe();
-
- final String obj = visitValueExpression(objExpr, bindingVar);
-
- // log.service -> binding.log().getService()
- if (objExpr instanceof VariableExpression
- && "log".equals(((VariableExpression) objExpr).getName())) {
- return visitLogProperty(propName, bindingVar);
- }
-
- // For parsed access and nested map access, use getAt()
- if (isSafe) {
- return "(" + obj + " == null ? null : getAt(" + obj + ", \"" + propName + "\"))";
- }
-
- return "getAt(" + obj + ", \"" + propName + "\")";
- }
-
- private String visitLogProperty(final String propName, final String bindingVar) {
- switch (propName) {
- case "service":
- return bindingVar + ".log().getService()";
- case "serviceInstance":
- return bindingVar + ".log().getServiceInstance()";
- case "endpoint":
- return bindingVar + ".log().getEndpoint()";
- case "timestamp":
- return bindingVar + ".log().getTimestamp()";
- default:
- return bindingVar + ".log().get" + capitalize(propName) + "()";
- }
- }
-
- private String visitCast(final CastExpression expr, final String bindingVar) {
- final String inner = visitValueExpression(expr.getExpression(), bindingVar);
- final String typeName = expr.getType().getName();
-
- switch (typeName) {
- case "java.lang.String":
- case "String":
- return "String.valueOf(" + inner + ")";
- case "java.lang.Long":
- case "Long":
- case "long":
- return "toLong(" + inner + ")";
- case "java.lang.Integer":
- case "Integer":
- case "int":
- return "toInt(" + inner + ")";
- case "java.lang.Boolean":
- case "Boolean":
- case "boolean":
- return "toBoolean(" + inner + ")";
- default:
- return "((" + typeName + ") " + inner + ")";
- }
- }
-
- private String visitMethodCallValue(final MethodCallExpression mce, final String bindingVar) {
- final String methodName = mce.getMethodAsString();
- final Expression objExpr = mce.getObjectExpression();
- final ArgumentListExpression args = toArgList(mce.getArguments());
- final boolean isSafe = mce.isSafe();
-
- // tag("KEY") on filterSpec -> filterSpec.tag("KEY")
- if ("tag".equals(methodName) && isThisOrImplicit(objExpr)) {
- final List<Expression> argExprs = args.getExpressions();
- if (!argExprs.isEmpty() && argExprs.get(0) instanceof ConstantExpression) {
- return "filterSpec.tag(\""
- + MalToJavaTranspiler.escapeJava(argExprs.get(0).getText()) + "\")";
- }
- }
-
- // ProcessRegistry.generateVirtualLocalProcess(...)
- if (objExpr instanceof VariableExpression
- && STATIC_TYPES.contains(((VariableExpression) objExpr).getName())) {
- final StringBuilder sb = new StringBuilder();
- sb.append("org.apache.skywalking.oap.meter.analyzer.dsl.registry.ProcessRegistry.");
- sb.append(methodName).append("(");
- final List<Expression> argExprs = args.getExpressions();
- for (int i = 0; i < argExprs.size(); i++) {
- if (i > 0) {
- sb.append(", ");
- }
- sb.append(visitValueExpression(argExprs.get(i), bindingVar));
- }
- sb.append(")");
- return sb.toString();
- }
-
- // toString(), trim() on safe navigation chain
- if ("toString".equals(methodName) || "trim".equals(methodName)) {
- final String obj = visitValueExpression(objExpr, bindingVar);
- if (isSafe) {
- return "(" + obj + " == null ? null : " + obj + "." + methodName + "())";
- }
- return obj + "." + methodName + "()";
- }
-
- // Generic method call
- final String obj = visitValueExpression(objExpr, bindingVar);
- final StringBuilder sb = new StringBuilder();
- if (isSafe) {
- sb.append("(").append(obj).append(" == null ? null : ");
- }
- sb.append(obj).append(".").append(methodName).append("(");
- final List<Expression> argExprs = args.getExpressions();
- for (int i = 0; i < argExprs.size(); i++) {
- if (i > 0) {
- sb.append(", ");
- }
- sb.append(visitValueExpression(argExprs.get(i), bindingVar));
- }
- sb.append(")");
- if (isSafe) {
- sb.append(")");
- }
- return sb.toString();
- }
-
- private String visitGString(final GStringExpression expr, final String bindingVar) {
- final List<ConstantExpression> strings = expr.getStrings();
- final List<Expression> values = expr.getValues();
-
- final StringBuilder sb = new StringBuilder();
- for (int i = 0; i < strings.size(); i++) {
- final String text = strings.get(i).getText();
- if (!text.isEmpty()) {
- if (sb.length() > 0) {
- sb.append(" + ");
- }
- sb.append("\"").append(MalToJavaTranspiler.escapeJava(text)).append("\"");
- }
- if (i < values.size()) {
- final String val = visitValueExpression(values.get(i), bindingVar);
- if (sb.length() > 0) {
- sb.append(" + ");
- }
- sb.append(val);
- }
- }
- return sb.length() > 0 ? sb.toString() : "\"\"";
- }
-
- private String visitMapExpression(final MapExpression expr, final String bindingVar) {
- final List<MapEntryExpression> entries = expr.getMapEntryExpressions();
- if (entries.isEmpty()) {
- return "java.util.Collections.emptyMap()";
- }
- if (entries.size() == 1) {
- final MapEntryExpression e = entries.get(0);
- return "Map.of(" + visitValueExpression(e.getKeyExpression(), bindingVar)
- + ", " + visitValueExpression(e.getValueExpression(), bindingVar) + ")";
- }
- final StringBuilder sb = new StringBuilder("Map.of(");
- for (int i = 0; i < entries.size(); i++) {
- if (i > 0) {
- sb.append(", ");
- }
- final MapEntryExpression e = entries.get(i);
- sb.append(visitValueExpression(e.getKeyExpression(), bindingVar));
- sb.append(", ");
- sb.append(visitValueExpression(e.getValueExpression(), bindingVar));
- }
- sb.append(")");
- return sb.toString();
- }
-
- // ---- Helpers ----
-
- private List<Statement> flattenStatements(final Statement stmt) {
- if (stmt instanceof BlockStatement) {
- return ((BlockStatement) stmt).getStatements();
- }
- return List.of(stmt);
- }
-
- private boolean isThisOrImplicit(final Expression expr) {
- if (expr instanceof VariableExpression) {
- final String name = ((VariableExpression) expr).getName();
- return "this".equals(name);
- }
- return false;
- }
-
- private String lambdaVarFor(final String methodName) {
- switch (methodName) {
- case "extractor": return "ext";
- case "sink": return "s";
- case "slowSql": return "sql";
- case "sampledTrace": return "st";
- case "metrics": return "m";
- case "sampler": return "sp";
- case "text": return "tp";
- default: return "x";
- }
- }
-
- private String childReceiverFor(final String lambdaVar) {
- return lambdaVar;
- }
-
- private ArgumentListExpression toArgList(final Expression args) {
- if (args instanceof ArgumentListExpression) {
- return (ArgumentListExpression) args;
- }
- if (args instanceof TupleExpression) {
- final ArgumentListExpression ale = new ArgumentListExpression();
- for (Expression e : ((TupleExpression) args).getExpressions()) {
- ale.addExpression(e);
- }
- return ale;
- }
- final ArgumentListExpression ale = new ArgumentListExpression();
- ale.addExpression(args);
- return ale;
- }
-
- private static String capitalize(final String s) {
- if (s == null || s.isEmpty()) {
- return s;
- }
- return Character.toUpperCase(s.charAt(0)) + s.substring(1);
- }
-
- private static void indent(final StringBuilder sb, final int level) {
- for (int i = 0; i < level; i++) {
- sb.append(" ");
- }
- }
-
- // ---- Compilation & Manifest ----
-
- /**
- * Compile all registered LAL sources using javax.tools.JavaCompiler.
- */
- public void compileAll(final File sourceDir, final File outputDir,
- final String classpath) throws IOException {
- if (lalSources.isEmpty()) {
- log.info("No LAL sources to compile.");
- return;
- }
-
- final String packageDir = GENERATED_PACKAGE.replace('.', File.separatorChar);
- final File srcPkgDir = new File(sourceDir, packageDir);
- if (!srcPkgDir.exists() && !srcPkgDir.mkdirs()) {
- throw new IOException("Failed to create source dir: " + srcPkgDir);
- }
- if (!outputDir.exists() && !outputDir.mkdirs()) {
- throw new IOException("Failed to create output dir: " + outputDir);
- }
-
- final List<File> javaFiles = new ArrayList<>();
- for (Map.Entry<String, String> entry : lalSources.entrySet()) {
- final File javaFile = new File(srcPkgDir, entry.getKey() + ".java");
- Files.writeString(javaFile.toPath(), entry.getValue());
- javaFiles.add(javaFile);
- }
-
- final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
- if (compiler == null) {
- throw new IllegalStateException("No Java compiler available — requires JDK");
- }
-
- final StringWriter errorWriter = new StringWriter();
-
- try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) {
- final Iterable<? extends JavaFileObject> compilationUnits =
- fileManager.getJavaFileObjectsFromFiles(javaFiles);
-
- final List<String> options = Arrays.asList(
- "-d", outputDir.getAbsolutePath(),
- "-classpath", classpath
- );
-
- final JavaCompiler.CompilationTask task = compiler.getTask(
- errorWriter, fileManager, null, options, null, compilationUnits);
-
- final boolean success = task.call();
- if (!success) {
- for (Map.Entry<String, String> entry : lalSources.entrySet()) {
- log.error("Generated source for {}:\n{}", entry.getKey(), entry.getValue());
- }
- throw new RuntimeException(
- "Java compilation failed for " + javaFiles.size() + " LAL sources:\n"
- + errorWriter);
- }
- }
-
- log.info("Compiled {} LAL sources to {}", lalSources.size(), outputDir);
- }
-
- /**
- * Write lal-expressions.txt manifest: hash=FQCN format.
- */
- public void writeManifest(final File outputDir) throws IOException {
- final File manifestDir = new File(outputDir, "META-INF");
- if (!manifestDir.exists() && !manifestDir.mkdirs()) {
- throw new IOException("Failed to create META-INF dir: " + manifestDir);
- }
-
- final List<String> lines = hashToClass.entrySet().stream()
- .map(e -> e.getKey() + "=" + e.getValue())
- .sorted()
- .collect(Collectors.toList());
- Files.write(new File(manifestDir, "lal-expressions.txt").toPath(), lines);
- log.info("Wrote lal-expressions.txt with {} entries", lines.size());
- }
-}
diff --git a/oap-server/analyzer/lal-transpiler/src/test/java/org/apache/skywalking/oap/server/transpiler/lal/LalToJavaTranspilerTest.java b/oap-server/analyzer/lal-transpiler/src/test/java/org/apache/skywalking/oap/server/transpiler/lal/LalToJavaTranspilerTest.java
deleted file mode 100644
index 4a580a4bcd..0000000000
--- a/oap-server/analyzer/lal-transpiler/src/test/java/org/apache/skywalking/oap/server/transpiler/lal/LalToJavaTranspilerTest.java
+++ /dev/null
@@ -1,678 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.skywalking.oap.server.transpiler.lal;
-
-import java.io.File;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.List;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
-
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-class LalToJavaTranspilerTest {
-
- private LalToJavaTranspiler transpiler;
-
- @BeforeEach
- void setUp() {
- transpiler = new LalToJavaTranspiler();
- }
-
- // ---- Class Structure ----
-
- @Test
- void classStructure() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter { json {} }");
- assertNotNull(java);
-
- assertTrue(java.contains("package " + LalToJavaTranspiler.GENERATED_PACKAGE),
- "Should have correct package");
- assertTrue(java.contains("public class LalExpr_test implements LalExpression"),
- "Should implement LalExpression");
- assertTrue(java.contains("public void execute(FilterSpec filterSpec, Binding binding)"),
- "Should have execute method");
- }
-
- @Test
- void helperMethodsPresent() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter { json {} }");
- assertNotNull(java);
-
- assertTrue(java.contains("private static Object getAt(Object obj, String key)"),
- "Should have getAt helper");
- assertTrue(java.contains("private static long toLong(Object obj)"),
- "Should have toLong helper");
- assertTrue(java.contains("private static int toInt(Object obj)"),
- "Should have toInt helper");
- assertTrue(java.contains("private static boolean toBoolean(Object obj)"),
- "Should have toBoolean helper");
- assertTrue(java.contains("private static boolean isTruthy(Object obj)"),
- "Should have isTruthy helper");
- assertTrue(java.contains("private static boolean isNonEmptyString(Object obj)"),
- "Should have isNonEmptyString helper");
- }
-
- @Test
- void importsPresent() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter { json {} }");
- assertNotNull(java);
-
- assertTrue(java.contains("import org.apache.skywalking.oap.log.analyzer.dsl.Binding;"),
- "Should import Binding");
- assertTrue(java.contains("import org.apache.skywalking.oap.log.analyzer.dsl.LalExpression;"),
- "Should import LalExpression");
- assertTrue(java.contains("import org.apache.skywalking.oap.log.analyzer.dsl.spec.filter.FilterSpec;"),
- "Should import FilterSpec");
- }
-
- // ---- filter {} Unwrapping ----
-
- @Test
- void filterUnwrap() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter { json {} }");
- assertNotNull(java);
-
- assertTrue(java.contains("filterSpec.json()"),
- "Should unwrap filter block and call json() on filterSpec");
- assertTrue(!java.contains("filterSpec.filter("),
- "Should NOT emit filterSpec.filter() call");
- }
-
- // ---- json {} ----
-
- @Test
- void jsonEmptyBlock() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter { json {} }");
- assertNotNull(java);
-
- assertTrue(java.contains("filterSpec.json();"),
- "Should emit no-arg json() call");
- }
-
- // ---- text { regexp $/pattern/$ } ----
-
- @Test
- void textWithRegexp() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter { text { regexp $/(?<timestamp>\\d+)\\s+(?<service>.+)/$ } }");
- assertNotNull(java);
-
- assertTrue(java.contains("filterSpec.text(tp -> {"),
- "Should emit text with Consumer lambda");
- assertTrue(java.contains("tp.regexp("),
- "Should call regexp on text parser spec");
- }
-
- // ---- extractor { ... } ----
-
- @Test
- void extractorWithService() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " extractor {\n" +
- " service parsed.service as String\n" +
- " }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("filterSpec.extractor(ext -> {"),
- "Should emit extractor with Consumer lambda");
- assertTrue(java.contains("ext.service("),
- "Should call service on extractor spec");
- assertTrue(java.contains("String.valueOf("),
- "Should use String.valueOf for 'as String' cast");
- assertTrue(java.contains("getAt(binding.parsed(), \"service\")"),
- "Should use getAt for parsed.service");
- }
-
- @Test
- void extractorWithMultipleFields() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " extractor {\n" +
- " service parsed.service as String\n" +
- " instance parsed.instance as String\n" +
- " endpoint parsed.endpoint as String\n" +
- " layer parsed.layer as String\n" +
- " timestamp parsed.time as String\n" +
- " traceId parsed.traceId as String\n" +
- " }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("ext.service(String.valueOf(getAt(binding.parsed(), \"service\")))"),
- "Should extract service");
- assertTrue(java.contains("ext.instance(String.valueOf(getAt(binding.parsed(), \"instance\")))"),
- "Should extract instance");
- assertTrue(java.contains("ext.endpoint(String.valueOf(getAt(binding.parsed(), \"endpoint\")))"),
- "Should extract endpoint");
- assertTrue(java.contains("ext.layer(String.valueOf(getAt(binding.parsed(), \"layer\")))"),
- "Should extract layer");
- assertTrue(java.contains("ext.timestamp(String.valueOf(getAt(binding.parsed(), \"time\")))"),
- "Should extract timestamp");
- assertTrue(java.contains("ext.traceId(String.valueOf(getAt(binding.parsed(), \"traceId\")))"),
- "Should extract traceId");
- }
-
- // ---- sink { ... } ----
-
- @Test
- void sinkWithEnforcer() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " sink {\n" +
- " enforcer {}\n" +
- " }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("filterSpec.sink(s -> {"),
- "Should emit sink with Consumer lambda");
- assertTrue(java.contains("s.enforcer();"),
- "Should emit no-arg enforcer() on sink spec");
- }
-
- @Test
- void sinkWithDropper() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " sink {\n" +
- " dropper {}\n" +
- " }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("s.dropper();"),
- "Should emit no-arg dropper() on sink spec");
- }
-
- @Test
- void sinkWithSampler() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " sink {\n" +
- " sampler {\n" +
- " rateLimit('abc')\n" +
- " }\n" +
- " }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("s.sampler(sp -> {"),
- "Should emit sampler with Consumer lambda");
- assertTrue(java.contains("sp.rateLimit(\"abc\")"),
- "Should call rateLimit on sampler spec");
- }
-
- // ---- abort {} ----
-
- @Test
- void abortBlock() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " abort {}\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("filterSpec.abort();"),
- "Should emit no-arg abort() call");
- }
-
- // ---- parsed access ----
-
- @Test
- void parsedPropertyAccess() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " extractor { service parsed.service as String }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("binding.parsed()"),
- "Should translate 'parsed' to binding.parsed()");
- assertTrue(java.contains("getAt(binding.parsed(), \"service\")"),
- "Should use getAt for property access");
- }
-
- @Test
- void parsedNestedAccess() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " extractor { service parsed.data.serviceName as String }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("getAt(getAt(binding.parsed(), \"data\"), \"serviceName\")"),
- "Should translate nested parsed access to nested getAt()");
- }
-
- // ---- Safe navigation (?.) ----
-
- @Test
- void safeNavigationOnParsed() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " extractor { service parsed?.service as String }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("== null ? null : getAt("),
- "Should translate ?. to null-safe ternary with getAt");
- }
-
- // ---- as Cast ----
-
- @Test
- void castAsString() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " extractor { service parsed.service as String }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("String.valueOf("),
- "Should translate 'as String' to String.valueOf()");
- }
-
- @Test
- void castAsLong() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " extractor { timestamp parsed.time as Long }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("toLong("),
- "Should translate 'as Long' to toLong()");
- }
-
- // ---- log access ----
-
- @Test
- void logPropertyAccess() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " extractor { service log.service }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("binding.log().getService()"),
- "Should translate log.service to binding.log().getService()");
- }
-
- // ---- if/else ----
-
- @Test
- void ifStatement() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " if (parsed.level == 'ERROR') {\n" +
- " abort {}\n" +
- " }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("if (\"ERROR\".equals(getAt(binding.parsed(), \"level\"))"),
- "Should translate == with constant on left for null-safety");
- assertTrue(java.contains("filterSpec.abort()"),
- "Should emit abort in if body");
- }
-
- @Test
- void ifElseStatement() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " if (parsed.type == 'access') {\n" +
- " extractor { layer 'HTTP' }\n" +
- " } else {\n" +
- " extractor { layer 'GENERAL' }\n" +
- " }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("if (\"access\".equals(getAt(binding.parsed(), \"type\"))"),
- "Should have if condition");
- assertTrue(java.contains("} else {"),
- "Should have else block");
- }
-
- @Test
- void ifElseIfStatement() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " if (parsed.type == 'a') {\n" +
- " extractor { layer 'A' }\n" +
- " } else if (parsed.type == 'b') {\n" +
- " extractor { layer 'B' }\n" +
- " } else {\n" +
- " extractor { layer 'C' }\n" +
- " }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("} else if ("),
- "Should produce else-if chain");
- }
-
- // ---- Condition operators ----
-
- @Test
- void notEqualCondition() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " if (parsed.status != 'ok') { abort {} }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("!\"ok\".equals(getAt(binding.parsed(), \"status\"))"),
- "Should translate != with negated .equals()");
- }
-
- @Test
- void logicalAndCondition() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " if (parsed.a == 'x' && parsed.b == 'y') { abort {} }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("\"x\".equals(getAt(binding.parsed(), \"a\")) && \"y\".equals(getAt(binding.parsed(), \"b\"))"),
- "Should translate && with .equals() on both sides");
- }
-
- @Test
- void logicalOrCondition() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " if (parsed.a == 'x' || parsed.a == 'y') { abort {} }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("\"x\".equals(") && java.contains("|| \"y\".equals("),
- "Should translate || correctly");
- }
-
- @Test
- void truthinessCondition() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " if (parsed.value) { abort {} }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("isTruthy(getAt(binding.parsed(), \"value\"))"),
- "Should translate bare expression to isTruthy()");
- }
-
- @Test
- void negationCondition() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " if (!parsed.value) { abort {} }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("!isTruthy(getAt(binding.parsed(), \"value\"))"),
- "Should translate !expr to negated isTruthy()");
- }
-
- @Test
- void lessThanCondition() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " if (parsed.code < 400) { abort {} }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("toInt(getAt(binding.parsed(), \"code\")) < 400"),
- "Should translate < with toInt on left");
- }
-
- @Test
- void greaterThanEqualCondition() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " if (parsed.code >= 500) { abort {} }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("toInt(getAt(binding.parsed(), \"code\")) >= 500"),
- "Should translate >= with toInt on left");
- }
-
- // ---- GString interpolation ----
-
- @Test
- void gstringInterpolation() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " extractor { service \"svc::${parsed.name}\" as String }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("\"svc::\""),
- "Should have string prefix");
- assertTrue(java.contains("getAt(binding.parsed(), \"name\")"),
- "Should have parsed access in interpolation");
- }
-
- // ---- tag(Map) ----
-
- @Test
- void tagWithMapLiteral() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " extractor {\n" +
- " tag(status: parsed.status as String)\n" +
- " }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("ext.tag("),
- "Should call tag on extractor spec");
- }
-
- // ---- slowSql / sampledTrace / metrics ----
-
- @Test
- void slowSqlBlock() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " extractor {\n" +
- " layer 'MYSQL'\n" +
- " service parsed.service as String\n" +
- " slowSql {\n" +
- " id parsed.id as String\n" +
- " statement parsed.statement as String\n" +
- " latency parsed.latency as Long\n" +
- " }\n" +
- " }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("ext.slowSql(sql -> {"),
- "Should emit slowSql with Consumer lambda, var 'sql'");
- assertTrue(java.contains("sql.id("),
- "Should call id on slowSql spec");
- assertTrue(java.contains("sql.statement("),
- "Should call statement on slowSql spec");
- assertTrue(java.contains("sql.latency(toLong("),
- "Should call latency with toLong");
- }
-
- @Test
- void sampledTraceBlock() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " extractor {\n" +
- " sampledTrace {\n" +
- " uri parsed.uri as String\n" +
- " latency parsed.latency as Long\n" +
- " }\n" +
- " }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("ext.sampledTrace(st -> {"),
- "Should emit sampledTrace with Consumer lambda, var 'st'");
- assertTrue(java.contains("st.uri("),
- "Should call uri on sampledTrace spec");
- }
-
- @Test
- void metricsBlock() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " extractor {\n" +
- " metrics {\n" +
- " name 'log_count'\n" +
- " value 1\n" +
- " }\n" +
- " }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("ext.metrics(m -> {"),
- "Should emit metrics with Consumer lambda, var 'm'");
- assertTrue(java.contains("m.name(\"log_count\")"),
- "Should call name on metrics spec");
- assertTrue(java.contains("m.value(1)"),
- "Should call value on metrics spec");
- }
-
- // ---- Complete LAL Script ----
-
- @Test
- void completeScript() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " extractor {\n" +
- " service parsed.service as String\n" +
- " instance parsed.instance as String\n" +
- " layer parsed.layer as String\n" +
- " timestamp parsed.time as String\n" +
- " }\n" +
- " sink {\n" +
- " enforcer {}\n" +
- " }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("filterSpec.json();"),
- "Should have json() call");
- assertTrue(java.contains("filterSpec.extractor(ext -> {"),
- "Should have extractor block");
- assertTrue(java.contains("filterSpec.sink(s -> {"),
- "Should have sink block");
- assertTrue(java.contains("s.enforcer();"),
- "Should have enforcer in sink");
- }
-
- // ---- tag("KEY") as value ----
-
- @Test
- void tagAsValue() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " if (tag('status') == 'error') { abort {} }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("filterSpec.tag(\"status\")"),
- "Should call tag on filterSpec");
- }
-
- // ---- rateLimit ----
-
- @Test
- void rateLimitWithClosureArg() {
- final String java = transpiler.transpile("LalExpr_test",
- "filter {\n" +
- " json {}\n" +
- " sink {\n" +
- " sampler {\n" +
- " rateLimit('myId') { rpm 5 }\n" +
- " }\n" +
- " }\n" +
- "}");
- assertNotNull(java);
-
- assertTrue(java.contains("sp.rateLimit(\"myId\", rls -> {"),
- "Should emit rateLimit with id and closure lambda");
- assertTrue(java.contains("rls.rpm(5)"),
- "Should call rpm on rate limit spec");
- }
-
- // ---- Manifest ----
-
- @Test
- void manifest(@TempDir Path tempDir) throws Exception {
- final String source = transpiler.transpile("LalExpr_a", "filter { json {} }");
- transpiler.register("LalExpr_a", "abc123hash", source);
-
- final File outputDir = tempDir.toFile();
- transpiler.writeManifest(outputDir);
-
- final File manifest = new File(outputDir, "META-INF/lal-expressions.txt");
- assertTrue(manifest.exists(), "Manifest file should exist");
-
- final List<String> lines = Files.readAllLines(manifest.toPath());
- assertTrue(lines.stream().anyMatch(l -> l.contains("abc123hash") &&
- l.contains(LalToJavaTranspiler.GENERATED_PACKAGE + ".LalExpr_a")),
- "Should contain hash=FQCN mapping");
- }
-}
diff --git a/oap-server/analyzer/log-analyzer-v2/pom.xml b/oap-server/analyzer/log-analyzer-v2/pom.xml
deleted file mode 100644
index 6456f8209f..0000000000
--- a/oap-server/analyzer/log-analyzer-v2/pom.xml
+++ /dev/null
@@ -1,72 +0,0 @@
-<?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
deleted file mode 100644
index b3f4e4c977..0000000000
--- a/oap-server/analyzer/log-analyzer-v2/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/DSL.java
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * 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/CLAUDE.md b/oap-server/analyzer/log-analyzer/CLAUDE.md
new file mode 100644
index 0000000000..a7b60c4a8e
--- /dev/null
+++ b/oap-server/analyzer/log-analyzer/CLAUDE.md
@@ -0,0 +1,128 @@
+# LAL Compiler
+
+Compiles LAL (Log Analysis Language) scripts into `LalExpression` implementation classes at runtime using ANTLR4 parsing and Javassist bytecode generation.
+
+## Compilation Workflow
+
+```
+LAL DSL string
+ → LALScriptParser.parse(dsl) [ANTLR4 lexer/parser → listener]
+ → LALScriptModel (immutable AST)
+ → LALClassGenerator.compileFromModel(model)
+ Phase 1: collectConsumers(model) — pre-scan for blocks needing Consumer callbacks
+ Phase 2: compileConsumerClass() — generate each consumer as separate Javassist class
+ Phase 3: classPool.makeClass() — create main class implementing LalExpression
+ Phase 4: generateExecuteMethod() — emit Java source referencing this._consumerN fields
+ Phase 5: ctClass.toClass(LalExpressionPackageHolder.class) + wire consumer fields
+ → LalExpression instance
+```
+
+The generated class implements:
+```java
+void execute(Object filterSpec, Object binding)
+ // cast internally to FilterSpec and Binding
+```
+
+## File Structure
+
+```
+oap-server/analyzer/log-analyzer/
+ src/main/antlr4/.../LALLexer.g4 — ANTLR4 lexer grammar
+ src/main/antlr4/.../LALParser.g4 — ANTLR4 parser grammar
+
+ src/main/java/.../compiler/
+ LALScriptParser.java — ANTLR4 facade: DSL string → AST
+ LALScriptModel.java — Immutable AST model classes
+ LALClassGenerator.java — Javassist code generator
+ rt/
+ LalExpressionPackageHolder.java — Class loading anchor (empty marker)
+ BindingAware.java — Interface for consumers needing Binding access
+
+ src/test/java/.../compiler/
+ LALScriptParserTest.java — 8 parser tests
+ LALClassGeneratorTest.java — 6 generator tests
+```
+
+## Package & Class Naming
+
+| Component | Package / Name |
+|-----------|---------------|
+| Parser/Model/Generator | `org.apache.skywalking.oap.log.analyzer.compiler` |
+| Generated classes | `org.apache.skywalking.oap.log.analyzer.compiler.rt.LalExpr_<N>` |
+| Consumer classes | `org.apache.skywalking.oap.log.analyzer.compiler.rt.LalExpr_<N>_C<M>` |
+| Package holder | `org.apache.skywalking.oap.log.analyzer.compiler.rt.LalExpressionPackageHolder` |
+| Binding aware | `org.apache.skywalking.oap.log.analyzer.compiler.rt.BindingAware` |
+| Functional interface | `org.apache.skywalking.oap.log.analyzer.dsl.LalExpression` (in log-analyzer) |
+
+`<N>` is a global `AtomicInteger` counter. `<M>` is the consumer index within the script.
+
+## Consumer Pattern (BindingAware)
+
+LAL's FilterSpec API uses `Consumer` callbacks: `filterSpec.extractor(Consumer<ExtractorSpec>)`, `filterSpec.sink(Consumer<SinkSpec>)`, etc. Since Javassist cannot compile anonymous inner classes, consumers are pre-compiled as separate classes.
+
+Each consumer class implements both `java.util.function.Consumer` and `BindingAware`:
+- `BindingAware.setBinding(Binding)` — called before each FilterSpec method to inject the current Binding
+- `Consumer.accept(Object)` — casts to the specific Spec type and executes the block body
+
+The main class's `execute()` method emits:
+```java
+((BindingAware) this._consumer0).setBinding(binding);
+filterSpec.extractor(this._consumer0);
+```
+
+Consumer traversal order in `collectConsumers()` must exactly match the order in `generateFilterStatement()`.
+
+## Javassist Constraints
+
+- **No anonymous inner classes**: All `Consumer` callbacks pre-compiled as separate `CtClass` instances.
+- **No lambda expressions**: Same workaround as above.
+- **Spec class packages**: Parser specs are in `dsl.spec.parser.*` (not `dsl.spec.extractor.*`):
+ - `spec.parser.TextParserSpec`, `spec.parser.JsonParserSpec`, `spec.parser.YamlParserSpec`
+ - `spec.extractor.ExtractorSpec`
+ - `spec.extractor.slowsql.SlowSqlSpec`, `spec.extractor.sampledtrace.SampledTraceSpec`
+ - `spec.sink.SinkSpec`, `spec.sink.SamplerSpec`
+
+## Example
+
+**Input**: `filter { json {} extractor { service parsed.service as String } sink {} }`
+
+Three classes are generated:
+
+1. **`LalExpr_0_C0`** — Consumer for extractor block:
+ ```java
+ // implements Consumer, BindingAware
+ public void accept(Object arg) {
+ ExtractorSpec _t = (ExtractorSpec) arg;
+ _t.service(String.valueOf(getAt(binding.parsed(), "service")));
+ }
+ ```
+
+2. **`LalExpr_0`** — Main class implementing `LalExpression`:
+ ```java
+ public Consumer _consumer0; // wired after toClass()
+
+ public void execute(Object arg0, Object arg1) {
+ FilterSpec filterSpec = (FilterSpec) arg0;
+ Binding binding = (Binding) arg1;
+ filterSpec.json();
+ ((BindingAware) this._consumer0).setBinding(binding);
+ filterSpec.extractor(this._consumer0);
+ filterSpec.sink();
+ }
+ ```
+
+**Consumer allocation rules**:
+- `json {}` with no `abortOnFailure` → no consumer, emits `filterSpec.json()`
+- `json { abortOnFailure }` → allocates a consumer
+- `text { regexp '...' }` → allocates a consumer
+- `text {}` with no regexp → no consumer, emits `filterSpec.text()`
+- `extractor { ... }` → always allocates a consumer
+- `sink {}` empty → no consumer, emits `filterSpec.sink()`
+- `sink { enforcer {} }` → allocates a consumer
+
+## Dependencies
+
+All within this module (grammar, compiler, and runtime are merged):
+- ANTLR4 grammar → generates lexer/parser at build time
+- `LalExpression`, `Binding`, `FilterSpec`, all Spec classes — in `dsl` package of this module
+- `javassist` — bytecode generation
diff --git a/oap-server/analyzer/log-analyzer/pom.xml b/oap-server/analyzer/log-analyzer/pom.xml
index 576539cb85..ff811b1596 100644
--- a/oap-server/analyzer/log-analyzer/pom.xml
+++ b/oap-server/analyzer/log-analyzer/pom.xml
@@ -7,13 +7,14 @@
~ (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
+ ~ 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">
@@ -25,7 +26,6 @@
<modelVersion>4.0.0</modelVersion>
<artifactId>log-analyzer</artifactId>
- <packaging>jar</packaging>
<dependencies>
<dependency>
@@ -43,13 +43,34 @@
<artifactId>agent-analyzer</artifactId>
<version>${project.version}</version>
</dependency>
- <dependency>
- <groupId>org.apache.groovy</groupId>
- <artifactId>groovy</artifactId>
- </dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.antlr</groupId>
+ <artifactId>antlr4-runtime</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.javassist</groupId>
+ <artifactId>javassist</artifactId>
+ </dependency>
</dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.antlr</groupId>
+ <artifactId>antlr4-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>antlr</id>
+ <goals>
+ <goal>antlr4</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
</project>
diff --git a/oap-server/analyzer/log-analyzer/src/main/antlr4/org/apache/skywalking/lal/rt/grammar/LALLexer.g4 b/oap-server/analyzer/log-analyzer/src/main/antlr4/org/apache/skywalking/lal/rt/grammar/LALLexer.g4
new file mode 100644
index 0000000000..4df6f7113a
--- /dev/null
+++ b/oap-server/analyzer/log-analyzer/src/main/antlr4/org/apache/skywalking/lal/rt/grammar/LALLexer.g4
@@ -0,0 +1,175 @@
+/*
+ * 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.
+ *
+ */
+
+// Log Analysis Language lexer
+lexer grammar LALLexer;
+
+@Header {package org.apache.skywalking.lal.rt.grammar;}
+
+// Keywords - block structure
+FILTER: 'filter';
+TEXT: 'text';
+JSON: 'json';
+YAML: 'yaml';
+EXTRACTOR: 'extractor';
+SINK: 'sink';
+ABORT: 'abort';
+
+// Keywords - extractor statements
+SERVICE: 'service';
+INSTANCE: 'instance';
+ENDPOINT: 'endpoint';
+LAYER: 'layer';
+TRACE_ID: 'traceId';
+SEGMENT_ID: 'segmentId';
+SPAN_ID: 'spanId';
+TIMESTAMP: 'timestamp';
+TAG: 'tag';
+METRICS: 'metrics';
+SLOW_SQL: 'slowSql';
+SAMPLED_TRACE: 'sampledTrace';
+REGEXP: 'regexp';
+ABORT_ON_FAILURE: 'abortOnFailure';
+NAME: 'name';
+VALUE: 'value';
+LABELS: 'labels';
+ID: 'id';
+STATEMENT: 'statement';
+LATENCY: 'latency';
+URI: 'uri';
+REASON: 'reason';
+PROCESS_ID: 'processId';
+DEST_PROCESS_ID: 'destProcessId';
+DETECT_POINT: 'detectPoint';
+COMPONENT_ID: 'componentId';
+REPORT_SERVICE: 'reportService';
+
+// Keywords - sink statements
+SAMPLER: 'sampler';
+RATE_LIMIT: 'rateLimit';
+RPM: 'rpm';
+ENFORCER: 'enforcer';
+DROPPER: 'dropper';
+
+// Keywords - control flow
+IF: 'if';
+ELSE: 'else';
+
+// Keywords - type cast
+AS: 'as';
+STRING_TYPE: 'String';
+LONG_TYPE: 'Long';
+INTEGER_TYPE: 'Integer';
+BOOLEAN_TYPE: 'Boolean';
+
+// Keywords - built-in references
+LOG: 'log';
+PARSED: 'parsed';
+
+// Keywords - utility class references
+PROCESS_REGISTRY: 'ProcessRegistry';
+
+// Comparison and logical operators
+DEQ: '==';
+NEQ: '!=';
+AND: '&&';
+OR: '||';
+NOT: '!';
+GT: '>';
+LT: '<';
+GTE: '>=';
+LTE: '<=';
+
+// Delimiters
+DOT: '.';
+COMMA: ',';
+COLON: ':';
+SEMI: ';';
+L_PAREN: '(';
+R_PAREN: ')';
+L_BRACE: '{';
+R_BRACE: '}';
+L_BRACKET: '[';
+R_BRACKET: ']';
+QUESTION: '?';
+ASSIGN: '=';
+
+// Arithmetic
+PLUS: '+';
+MINUS: '-';
+STAR: '*';
+SLASH: '/';
+
+// Literals
+TRUE: 'true';
+FALSE: 'false';
+NULL: 'null';
+
+NUMBER
+ : Digit+ ('.' Digit+)?
+ ;
+
+// String literal: single or double quoted
+STRING
+ : '\'' (~['\\\r\n] | EscapeSequence)* '\''
+ | '"' (~["\\\r\n] | EscapeSequence)* '"'
+ ;
+
+// Groovy-style slashy string for regex patterns: $/pattern/$
+SLASHY_STRING
+ : '$/' .*? '/$'
+ ;
+
+// Comments
+LINE_COMMENT
+ : '//' ~[\r\n]* -> channel(HIDDEN)
+ ;
+
+BLOCK_COMMENT
+ : '/*' .*? '*/' -> channel(HIDDEN)
+ ;
+
+// Whitespace
+WS
+ : [ \t\r\n]+ -> channel(HIDDEN)
+ ;
+
+// Identifiers
+IDENTIFIER
+ : Letter LetterOrDigit*
+ ;
+
+// Fragments
+fragment EscapeSequence
+ : '\\' [btnfr"'\\]
+ | '\\' ([0-3]? [0-7])? [0-7]
+ | '\\' . // catch-all for regex escapes like \d, \w, \s
+ ;
+
+fragment Digit
+ : [0-9]
+ ;
+
+fragment Letter
+ : [a-zA-Z_]
+ ;
+
+fragment LetterOrDigit
+ : Letter
+ | [0-9]
+ ;
diff --git a/oap-server/analyzer/log-analyzer/src/main/antlr4/org/apache/skywalking/lal/rt/grammar/LALParser.g4 b/oap-server/analyzer/log-analyzer/src/main/antlr4/org/apache/skywalking/lal/rt/grammar/LALParser.g4
new file mode 100644
index 0000000000..b381aa1573
--- /dev/null
+++ b/oap-server/analyzer/log-analyzer/src/main/antlr4/org/apache/skywalking/lal/rt/grammar/LALParser.g4
@@ -0,0 +1,464 @@
+/*
+ * 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.
+ *
+ */
+
+// Log Analysis Language parser
+//
+// Covers LAL DSL patterns:
+// filter { parser {} extractor {} sink {} }
+// if (tag("LOG_KIND") == "NGINX_ACCESS_LOG") { ... }
+// text { regexp $/pattern/$ }
+// json { abortOnFailure true }
+// extractor { service parsed.service as String; tag 'key': value }
+// metrics { name "metric_name"; value 1; labels key: val }
+// slowSql { id parsed.id as String; statement parsed.statement as String; latency parsed.query_time as Long }
+// sampledTrace { latency parsed.latency as Long; uri parsed.uri as String; ... }
+// sink { sampler { rateLimit("id") { rpm 6000 } } }
+parser grammar LALParser;
+
+@Header {package org.apache.skywalking.lal.rt.grammar;}
+
+options { tokenVocab=LALLexer; }
+
+// ==================== Top-level ====================
+
+root
+ : filterBlock EOF
+ ;
+
+// ==================== Filter block ====================
+
+filterBlock
+ : FILTER L_BRACE filterContent R_BRACE
+ ;
+
+filterContent
+ : filterStatement*
+ ;
+
+filterStatement
+ : parserBlock
+ | extractorBlock
+ | sinkBlock
+ | ifStatement
+ | abortBlock
+ ;
+
+// ==================== Parser blocks ====================
+
+parserBlock
+ : textBlock
+ | jsonBlock
+ | yamlBlock
+ ;
+
+textBlock
+ : TEXT L_BRACE textContent R_BRACE
+ ;
+
+textContent
+ : (regexpStatement | abortOnFailureStatement)*
+ ;
+
+regexpStatement
+ : REGEXP regexpPattern
+ ;
+
+regexpPattern
+ : SLASHY_STRING
+ | STRING
+ ;
+
+jsonBlock
+ : JSON L_BRACE jsonContent R_BRACE
+ ;
+
+jsonContent
+ : abortOnFailureStatement?
+ ;
+
+yamlBlock
+ : YAML L_BRACE yamlContent R_BRACE
+ ;
+
+yamlContent
+ : abortOnFailureStatement?
+ ;
+
+abortOnFailureStatement
+ : ABORT_ON_FAILURE boolValue
+ ;
+
+abortBlock
+ : ABORT L_BRACE R_BRACE
+ ;
+
+// ==================== Extractor block ====================
+
+extractorBlock
+ : EXTRACTOR L_BRACE extractorContent R_BRACE
+ ;
+
+extractorContent
+ : extractorStatement*
+ ;
+
+extractorStatement
+ : serviceStatement
+ | instanceStatement
+ | endpointStatement
+ | layerStatement
+ | traceIdStatement
+ | segmentIdStatement
+ | spanIdStatement
+ | timestampStatement
+ | tagStatement
+ | metricsBlock
+ | slowSqlBlock
+ | sampledTraceBlock
+ | ifStatement
+ ;
+
+serviceStatement
+ : SERVICE valueAccess typeCast?
+ ;
+
+instanceStatement
+ : INSTANCE valueAccess typeCast?
+ ;
+
+endpointStatement
+ : ENDPOINT valueAccess typeCast?
+ ;
+
+layerStatement
+ : LAYER valueAccess typeCast?
+ ;
+
+traceIdStatement
+ : TRACE_ID valueAccess typeCast?
+ ;
+
+segmentIdStatement
+ : SEGMENT_ID valueAccess typeCast?
+ ;
+
+spanIdStatement
+ : SPAN_ID valueAccess typeCast?
+ ;
+
+timestampStatement
+ : TIMESTAMP valueAccess typeCast? (COMMA STRING)?
+ ;
+
+tagStatement
+ : TAG tagMap
+ | TAG STRING COLON valueAccess typeCast?
+ ;
+
+tagMap
+ : anyIdentifier COLON valueAccess typeCast? (COMMA anyIdentifier COLON valueAccess typeCast?)*
+ ;
+
+// ==================== Metrics block ====================
+
+metricsBlock
+ : METRICS L_BRACE metricsContent R_BRACE
+ ;
+
+metricsContent
+ : metricsStatement*
+ ;
+
+metricsStatement
+ : metricsNameStatement
+ | metricsTimestampStatement
+ | metricsLabelsStatement
+ | metricsValueStatement
+ ;
+
+metricsNameStatement
+ : NAME valueAccess typeCast?
+ ;
+
+metricsTimestampStatement
+ : TIMESTAMP valueAccess typeCast?
+ ;
+
+metricsLabelsStatement
+ : LABELS labelMap
+ ;
+
+labelMap
+ : labelEntry (COMMA labelEntry)*
+ ;
+
+labelEntry
+ : anyIdentifier COLON valueAccess typeCast?
+ ;
+
+metricsValueStatement
+ : VALUE valueAccess typeCast?
+ ;
+
+// ==================== Slow SQL block ====================
+
+slowSqlBlock
+ : SLOW_SQL L_BRACE slowSqlContent R_BRACE
+ ;
+
+slowSqlContent
+ : slowSqlStatement*
+ ;
+
+slowSqlStatement
+ : slowSqlIdStatement
+ | slowSqlStatementStatement
+ | slowSqlLatencyStatement
+ ;
+
+slowSqlIdStatement
+ : ID valueAccess typeCast?
+ ;
+
+slowSqlStatementStatement
+ : STATEMENT valueAccess typeCast?
+ ;
+
+slowSqlLatencyStatement
+ : LATENCY valueAccess typeCast?
+ ;
+
+// ==================== Sampled trace block ====================
+
+sampledTraceBlock
+ : SAMPLED_TRACE L_BRACE sampledTraceContent R_BRACE
+ ;
+
+sampledTraceContent
+ : sampledTraceStatement*
+ ;
+
+sampledTraceStatement
+ : sampledTraceLatencyStatement
+ | sampledTraceUriStatement
+ | sampledTraceReasonStatement
+ | sampledTraceProcessIdStatement
+ | sampledTraceDestProcessIdStatement
+ | sampledTraceDetectPointStatement
+ | sampledTraceComponentIdStatement
+ | reportServiceStatement
+ | ifStatement
+ ;
+
+sampledTraceLatencyStatement
+ : LATENCY valueAccess typeCast?
+ ;
+
+sampledTraceUriStatement
+ : URI valueAccess typeCast?
+ ;
+
+sampledTraceReasonStatement
+ : REASON valueAccess typeCast?
+ ;
+
+sampledTraceProcessIdStatement
+ : PROCESS_ID valueAccess typeCast?
+ ;
+
+sampledTraceDestProcessIdStatement
+ : DEST_PROCESS_ID valueAccess typeCast?
+ ;
+
+sampledTraceDetectPointStatement
+ : DETECT_POINT valueAccess typeCast?
+ ;
+
+sampledTraceComponentIdStatement
+ : COMPONENT_ID valueAccess typeCast?
+ ;
+
+reportServiceStatement
+ : REPORT_SERVICE valueAccess typeCast?
+ ;
+
+// ==================== Sink block ====================
+
+sinkBlock
+ : SINK L_BRACE sinkContent R_BRACE
+ ;
+
+sinkContent
+ : sinkStatement*
+ ;
+
+sinkStatement
+ : samplerBlock
+ | enforcerStatement
+ | dropperStatement
+ | ifStatement
+ ;
+
+samplerBlock
+ : SAMPLER L_BRACE samplerContent R_BRACE
+ ;
+
+samplerContent
+ : (rateLimitBlock | ifStatement)*
+ ;
+
+rateLimitBlock
+ : RATE_LIMIT L_PAREN rateLimitId R_PAREN L_BRACE rateLimitContent R_BRACE
+ ;
+
+rateLimitId
+ : STRING
+ ;
+
+rateLimitContent
+ : RPM NUMBER
+ ;
+
+enforcerStatement
+ : ENFORCER L_BRACE R_BRACE
+ ;
+
+dropperStatement
+ : DROPPER L_BRACE R_BRACE
+ ;
+
+// ==================== Control flow ====================
+
+ifStatement
+ : IF L_PAREN condition R_PAREN L_BRACE
+ ifBody
+ R_BRACE
+ (ELSE IF L_PAREN condition R_PAREN L_BRACE
+ ifBody
+ R_BRACE)*
+ (ELSE L_BRACE
+ ifBody
+ R_BRACE)?
+ ;
+
+ifBody
+ : filterStatement*
+ | extractorStatement*
+ | sinkStatement*
+ | sampledTraceStatement*
+ | samplerContent
+ ;
+
+// ==================== Conditions ====================
+
+condition
+ : condition AND condition # condAnd
+ | condition OR condition # condOr
+ | NOT condition # condNot
+ | L_PAREN condition R_PAREN # condParen
+ | conditionExpr DEQ conditionExpr # condEq
+ | conditionExpr NEQ conditionExpr # condNeq
+ | conditionExpr GT conditionExpr # condGt
+ | conditionExpr LT conditionExpr # condLt
+ | conditionExpr GTE conditionExpr # condGte
+ | conditionExpr LTE conditionExpr # condLte
+ | conditionExpr # condSingle
+ ;
+
+conditionExpr
+ : valueAccess typeCast? # condValueAccess
+ | STRING # condString
+ | NUMBER # condNumber
+ | boolValue # condBool
+ | NULL # condNull
+ | functionInvocation # condFunctionCall
+ ;
+
+// ==================== Value access ====================
+
+// Accessing parsed values, log fields, and method calls:
+// parsed.level, parsed?.response?.responseCode?.value
+// log.service, log.timestamp, log.serviceInstance
+// tag("LOG_KIND")
+// ProcessRegistry.generateVirtualLocalProcess(...)
+
+valueAccess
+ : valueAccessPrimary (valueAccessSegment)*
+ ;
+
+valueAccessPrimary
+ : PARSED # valueParsed
+ | LOG # valueLog
+ | IDENTIFIER # valueIdentifier
+ | STRING # valueString
+ | NUMBER # valueNumber
+ | boolValue # valueBool
+ | NULL # valueNull
+ | functionInvocation # valueFunctionCall
+ ;
+
+valueAccessSegment
+ : DOT anyIdentifier # segmentField
+ | QUESTION DOT anyIdentifier # segmentSafeField
+ | DOT functionInvocation # segmentMethod
+ | QUESTION DOT functionInvocation # segmentSafeMethod
+ ;
+
+functionInvocation
+ : IDENTIFIER L_PAREN functionArgList? R_PAREN
+ ;
+
+functionArgList
+ : functionArg (COMMA functionArg)*
+ ;
+
+functionArg
+ : valueAccess typeCast?
+ | STRING
+ | NUMBER
+ | boolValue
+ | NULL
+ ;
+
+// ==================== Type cast ====================
+
+typeCast
+ : AS (STRING_TYPE | LONG_TYPE | INTEGER_TYPE | BOOLEAN_TYPE)
+ ;
+
+// ==================== Common ====================
+
+// Allows keywords to be used as identifiers in contexts like field names,
+// labels, and value access segments (e.g. parsed.service, parsed.layer).
+anyIdentifier
+ : IDENTIFIER
+ | SERVICE | INSTANCE | ENDPOINT | LAYER
+ | TRACE_ID | SEGMENT_ID | SPAN_ID | TIMESTAMP
+ | TAG | METRICS | SLOW_SQL | SAMPLED_TRACE
+ | REGEXP | ABORT_ON_FAILURE
+ | NAME | VALUE | LABELS
+ | ID | STATEMENT | LATENCY
+ | URI | REASON | PROCESS_ID | DEST_PROCESS_ID
+ | DETECT_POINT | COMPONENT_ID | REPORT_SERVICE
+ | SAMPLER | RATE_LIMIT | RPM | ENFORCER | DROPPER
+ | TEXT | JSON | YAML | FILTER | EXTRACTOR | SINK | ABORT
+ ;
+
+boolValue
+ : TRUE | FALSE
+ ;
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALClassGenerator.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALClassGenerator.java
new file mode 100644
index 0000000000..092d9f45f1
--- /dev/null
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALClassGenerator.java
@@ -0,0 +1,627 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.log.analyzer.compiler;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import javassist.ClassPool;
+import javassist.CtClass;
+import javassist.CtField;
+import javassist.CtNewConstructor;
+import javassist.CtNewMethod;
+import org.apache.skywalking.oap.log.analyzer.compiler.rt.LalExpressionPackageHolder;
+import org.apache.skywalking.oap.log.analyzer.dsl.LalExpression;
+
+/**
+ * Generates {@link LalExpression} implementation classes from
+ * {@link LALScriptModel} AST using Javassist bytecode generation.
+ *
+ * <p>Because Javassist cannot compile anonymous inner classes,
+ * Consumer callbacks are pre-compiled as separate classes and
+ * stored as fields on the main class.
+ */
+public final class LALClassGenerator {
+
+ private static final AtomicInteger CLASS_COUNTER = new AtomicInteger(0);
+
+ private static final String PACKAGE_PREFIX =
+ "org.apache.skywalking.oap.log.analyzer.compiler.rt.";
+
+ private static final String FILTER_SPEC =
+ "org.apache.skywalking.oap.log.analyzer.dsl.spec.filter.FilterSpec";
+ private static final String BINDING =
+ "org.apache.skywalking.oap.log.analyzer.dsl.Binding";
+ private static final String BINDING_PARSED =
+ "org.apache.skywalking.oap.log.analyzer.dsl.Binding.Parsed";
+
+ private final ClassPool classPool;
+
+ public LALClassGenerator() {
+ this(ClassPool.getDefault());
+ }
+
+ public LALClassGenerator(final ClassPool classPool) {
+ this.classPool = classPool;
+ }
+
+ /**
+ * Compiles a LAL DSL script into a LalExpression implementation.
+ */
+ public LalExpression compile(final String dsl) throws Exception {
+ final LALScriptModel model = LALScriptParser.parse(dsl);
+ return compileFromModel(model);
+ }
+
+ /**
+ * Compiles from a pre-parsed model.
+ */
+ public LalExpression compileFromModel(final LALScriptModel model) throws Exception {
+ final String className = PACKAGE_PREFIX + "LalExpr_"
+ + CLASS_COUNTER.getAndIncrement();
+
+ // Phase 1: Collect all consumer info in traversal order
+ final List<ConsumerInfo> consumers = new ArrayList<>();
+ collectConsumers(model.getStatements(), consumers);
+
+ // Phase 2: Compile consumer classes
+ final List<Object> consumerInstances = new ArrayList<>();
+ for (int i = 0; i < consumers.size(); i++) {
+ final String consumerName = className + "_C" + i;
+ final Object instance = compileConsumerClass(
+ consumerName, consumers.get(i));
+ consumerInstances.add(instance);
+ }
+
+ // Phase 3: Build main class with consumer fields
+ final CtClass ctClass = classPool.makeClass(className);
+ ctClass.addInterface(classPool.get(
+ "org.apache.skywalking.oap.log.analyzer.dsl.LalExpression"));
+
+ for (int i = 0; i < consumers.size(); i++) {
+ ctClass.addField(CtField.make(
+ "public java.util.function.Consumer _consumer" + i + ";",
+ ctClass));
+ }
+
+ ctClass.addConstructor(CtNewConstructor.defaultConstructor(ctClass));
+ addHelperMethods(ctClass);
+
+ // Phase 4: Generate execute method referencing consumer fields
+ final int[] counter = {0};
+ final String executeBody = generateExecuteMethod(model, counter);
+ ctClass.addMethod(CtNewMethod.make(executeBody, ctClass));
+
+ final Class<?> clazz = ctClass.toClass(LalExpressionPackageHolder.class);
+ ctClass.detach();
+ final LalExpression instance = (LalExpression) clazz
+ .getDeclaredConstructor().newInstance();
+
+ // Phase 5: Wire consumer fields
+ for (int i = 0; i < consumerInstances.size(); i++) {
+ clazz.getField("_consumer" + i).set(instance, consumerInstances.get(i));
+ }
+
+ return instance;
+ }
+
+ // ==================== Consumer info ====================
+
+ private static final class ConsumerInfo {
+ final String body;
+ final String castType;
+ final List<ConsumerInfo> subConsumers;
+
+ ConsumerInfo(final String body, final String castType) {
+ this.body = body;
+ this.castType = castType;
+ this.subConsumers = new ArrayList<>();
+ }
+ }
+
+ // ==================== Phase 1: Collect consumers ====================
+
+ private void collectConsumers(
+ final List<? extends LALScriptModel.FilterStatement> stmts,
+ final List<ConsumerInfo> consumers) {
+ for (final LALScriptModel.FilterStatement stmt : stmts) {
+ collectConsumerFromStatement(stmt, consumers);
+ }
+ }
+
+ private void collectConsumerFromStatement(
+ final LALScriptModel.FilterStatement stmt,
+ final List<ConsumerInfo> consumers) {
+ if (stmt instanceof LALScriptModel.TextParser) {
+ final LALScriptModel.TextParser tp = (LALScriptModel.TextParser) stmt;
+ if (tp.getRegexpPattern() != null) {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(" _t.regexp(\"")
+ .append(escapeJava(tp.getRegexpPattern()))
+ .append("\");\n");
+ consumers.add(new ConsumerInfo(sb.toString(),
+ "org.apache.skywalking.oap.log.analyzer.dsl"
+ + ".spec.parser.TextParserSpec"));
+ }
+ } else if (stmt instanceof LALScriptModel.JsonParser) {
+ if (((LALScriptModel.JsonParser) stmt).isAbortOnFailure()) {
+ consumers.add(new ConsumerInfo(
+ " _t.abortOnFailure();\n",
+ "org.apache.skywalking.oap.log.analyzer.dsl"
+ + ".spec.parser.JsonParserSpec"));
+ }
+ } else if (stmt instanceof LALScriptModel.YamlParser) {
+ if (((LALScriptModel.YamlParser) stmt).isAbortOnFailure()) {
+ consumers.add(new ConsumerInfo(
+ " _t.abortOnFailure();\n",
+ "org.apache.skywalking.oap.log.analyzer.dsl"
+ + ".spec.parser.YamlParserSpec"));
+ }
+ } else if (stmt instanceof LALScriptModel.ExtractorBlock) {
+ final LALScriptModel.ExtractorBlock block =
+ (LALScriptModel.ExtractorBlock) stmt;
+ final StringBuilder sb = new StringBuilder();
+ generateExtractorStatementsFlat(sb, block.getStatements());
+ consumers.add(new ConsumerInfo(sb.toString(),
+ "org.apache.skywalking.oap.log.analyzer.dsl"
+ + ".spec.extractor.ExtractorSpec"));
+ } else if (stmt instanceof LALScriptModel.SinkBlock) {
+ final LALScriptModel.SinkBlock sink = (LALScriptModel.SinkBlock) stmt;
+ if (!sink.getStatements().isEmpty()) {
+ final StringBuilder sb = new StringBuilder();
+ generateSinkStatementsFlat(sb, sink.getStatements());
+ consumers.add(new ConsumerInfo(sb.toString(),
+ "org.apache.skywalking.oap.log.analyzer.dsl"
+ + ".spec.sink.SinkSpec"));
+ }
+ } else if (stmt instanceof LALScriptModel.IfBlock) {
+ final LALScriptModel.IfBlock ifBlock = (LALScriptModel.IfBlock) stmt;
+ collectConsumers(ifBlock.getThenBranch(), consumers);
+ if (!ifBlock.getElseBranch().isEmpty()) {
+ collectConsumers(ifBlock.getElseBranch(), consumers);
+ }
+ }
+ }
+
+ // ==================== Flat code for consumer bodies ====================
+
+ private void generateExtractorStatementsFlat(
+ final StringBuilder sb,
+ final List<LALScriptModel.ExtractorStatement> stmts) {
+ for (final LALScriptModel.ExtractorStatement stmt : stmts) {
+ if (stmt instanceof LALScriptModel.FieldAssignment) {
+ final LALScriptModel.FieldAssignment field =
+ (LALScriptModel.FieldAssignment) stmt;
+ sb.append(" _t.").append(field.getFieldType().name().toLowerCase())
+ .append("(");
+ generateCastedValueAccess(sb, field.getValue(),
+ field.getCastType());
+ if (field.getFormatPattern() != null) {
+ sb.append(", \"")
+ .append(escapeJava(field.getFormatPattern()))
+ .append("\"");
+ }
+ sb.append(");\n");
+ } else if (stmt instanceof LALScriptModel.TagAssignment) {
+ final LALScriptModel.TagAssignment tag =
+ (LALScriptModel.TagAssignment) stmt;
+ if (tag.getTags().size() == 1) {
+ final Map.Entry<String, LALScriptModel.TagValue> entry =
+ tag.getTags().entrySet().iterator().next();
+ sb.append(" _t.tag(\"")
+ .append(escapeJava(entry.getKey())).append("\", ");
+ generateCastedValueAccess(sb, entry.getValue().getValue(),
+ entry.getValue().getCastType());
+ sb.append(");\n");
+ }
+ }
+ }
+ }
+
+ private void generateSinkStatementsFlat(
+ final StringBuilder sb,
+ final List<LALScriptModel.SinkStatement> stmts) {
+ for (final LALScriptModel.SinkStatement stmt : stmts) {
+ if (stmt instanceof LALScriptModel.EnforcerStatement) {
+ sb.append(" _t.enforcer();\n");
+ } else if (stmt instanceof LALScriptModel.DropperStatement) {
+ sb.append(" _t.dropper();\n");
+ }
+ }
+ }
+
+ // ==================== Phase 2: Compile consumer classes ====================
+
+ private Object compileConsumerClass(final String className,
+ final ConsumerInfo info) throws Exception {
+ final CtClass ctClass = classPool.makeClass(className);
+ ctClass.addInterface(classPool.get("java.util.function.Consumer"));
+ ctClass.addInterface(classPool.get(
+ PACKAGE_PREFIX + "BindingAware"));
+ ctClass.addConstructor(CtNewConstructor.defaultConstructor(ctClass));
+
+ ctClass.addField(CtField.make(
+ "private " + BINDING + " binding;", ctClass));
+
+ ctClass.addMethod(CtNewMethod.make(
+ "public void setBinding(" + BINDING + " b) {"
+ + " this.binding = b; }", ctClass));
+ ctClass.addMethod(CtNewMethod.make(
+ "public " + BINDING + " getBinding() {"
+ + " return this.binding; }", ctClass));
+
+ addHelperMethods(ctClass);
+
+ final String method = "public void accept(Object arg) {\n"
+ + " " + info.castType + " _t = (" + info.castType + ") arg;\n"
+ + info.body
+ + "}\n";
+ ctClass.addMethod(CtNewMethod.make(method, ctClass));
+
+ final Class<?> clazz = ctClass.toClass(LalExpressionPackageHolder.class);
+ ctClass.detach();
+ return clazz.getDeclaredConstructor().newInstance();
+ }
+
+ // ==================== Phase 4: Generate execute method ====================
+
+ private String generateExecuteMethod(final LALScriptModel model,
+ final int[] counter) {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("public void execute(Object arg0, Object arg1) {\n");
+ sb.append(" ").append(FILTER_SPEC).append(" filterSpec = (")
+ .append(FILTER_SPEC).append(") arg0;\n");
+ sb.append(" ").append(BINDING).append(" binding = (")
+ .append(BINDING).append(") arg1;\n");
+
+ for (final LALScriptModel.FilterStatement stmt
+ : model.getStatements()) {
+ generateFilterStatement(sb, stmt, counter);
+ }
+
+ sb.append("}\n");
+ return sb.toString();
+ }
+
+ private void generateFilterStatement(final StringBuilder sb,
+ final LALScriptModel.FilterStatement stmt,
+ final int[] counter) {
+ if (stmt instanceof LALScriptModel.TextParser) {
+ final LALScriptModel.TextParser tp = (LALScriptModel.TextParser) stmt;
+ if (tp.getRegexpPattern() != null) {
+ emitConsumerCall(sb, "filterSpec.text", counter);
+ } else {
+ sb.append(" filterSpec.text();\n");
+ }
+ } else if (stmt instanceof LALScriptModel.JsonParser) {
+ if (((LALScriptModel.JsonParser) stmt).isAbortOnFailure()) {
+ emitConsumerCall(sb, "filterSpec.json", counter);
+ } else {
+ sb.append(" filterSpec.json();\n");
+ }
+ } else if (stmt instanceof LALScriptModel.YamlParser) {
+ if (((LALScriptModel.YamlParser) stmt).isAbortOnFailure()) {
+ emitConsumerCall(sb, "filterSpec.yaml", counter);
+ } else {
+ sb.append(" filterSpec.yaml();\n");
+ }
+ } else if (stmt instanceof LALScriptModel.AbortStatement) {
+ sb.append(" filterSpec.abort();\n");
+ } else if (stmt instanceof LALScriptModel.ExtractorBlock) {
+ emitConsumerCall(sb, "filterSpec.extractor", counter);
+ } else if (stmt instanceof LALScriptModel.SinkBlock) {
+ final LALScriptModel.SinkBlock sink = (LALScriptModel.SinkBlock) stmt;
+ if (sink.getStatements().isEmpty()) {
+ sb.append(" filterSpec.sink();\n");
+ } else {
+ emitConsumerCall(sb, "filterSpec.sink", counter);
+ }
+ } else if (stmt instanceof LALScriptModel.IfBlock) {
+ generateIfBlock(sb, (LALScriptModel.IfBlock) stmt, counter);
+ }
+ }
+
+ private void emitConsumerCall(final StringBuilder sb,
+ final String methodPrefix,
+ final int[] counter) {
+ final int idx = counter[0]++;
+ sb.append(" ((")
+ .append(PACKAGE_PREFIX).append("BindingAware) this._consumer")
+ .append(idx).append(").setBinding(binding);\n");
+ sb.append(" ").append(methodPrefix)
+ .append("(this._consumer").append(idx).append(");\n");
+ }
+
+ private void generateIfBlock(final StringBuilder sb,
+ final LALScriptModel.IfBlock ifBlock,
+ final int[] counter) {
+ sb.append(" if (");
+ generateCondition(sb, ifBlock.getCondition());
+ sb.append(") {\n");
+ for (final LALScriptModel.FilterStatement s : ifBlock.getThenBranch()) {
+ generateFilterStatement(sb, s, counter);
+ }
+ sb.append(" }\n");
+ if (!ifBlock.getElseBranch().isEmpty()) {
+ sb.append(" else {\n");
+ for (final LALScriptModel.FilterStatement s
+ : ifBlock.getElseBranch()) {
+ generateFilterStatement(sb, s, counter);
+ }
+ sb.append(" }\n");
+ }
+ }
+
+ // ==================== Helper methods ====================
+
+ private void addHelperMethods(final CtClass ctClass) throws Exception {
+ ctClass.addMethod(CtNewMethod.make(
+ "private static Object getAt(Object obj, String key) {"
+ + " if (obj == null) return null;"
+ + " if (obj instanceof " + BINDING_PARSED + ")"
+ + " return ((" + BINDING_PARSED + ") obj).getAt(key);"
+ + " if (obj instanceof java.util.Map)"
+ + " return ((java.util.Map) obj).get(key);"
+ + " return null;"
+ + "}", ctClass));
+
+ ctClass.addMethod(CtNewMethod.make(
+ "private static long toLong(Object obj) {"
+ + " if (obj instanceof Number) return ((Number) obj).longValue();"
+ + " if (obj instanceof String) return Long.parseLong((String) obj);"
+ + " return 0L;"
+ + "}", ctClass));
+
+ ctClass.addMethod(CtNewMethod.make(
+ "private static int toInt(Object obj) {"
+ + " if (obj instanceof Number) return ((Number) obj).intValue();"
+ + " if (obj instanceof String) return Integer.parseInt((String) obj);"
+ + " return 0;"
+ + "}", ctClass));
+
+ ctClass.addMethod(CtNewMethod.make(
+ "private static boolean toBool(Object obj) {"
+ + " if (obj instanceof Boolean) return ((Boolean) obj).booleanValue();"
+ + " if (obj instanceof String)"
+ + " return Boolean.parseBoolean((String) obj);"
+ + " return obj != null;"
+ + "}", ctClass));
+
+ ctClass.addMethod(CtNewMethod.make(
+ "private static boolean isTruthy(Object obj) {"
+ + " if (obj == null) return false;"
+ + " if (obj instanceof Boolean)"
+ + " return ((Boolean) obj).booleanValue();"
+ + " if (obj instanceof String)"
+ + " return !((String) obj).isEmpty();"
+ + " if (obj instanceof Number)"
+ + " return ((Number) obj).doubleValue() != 0;"
+ + " return true;"
+ + "}", ctClass));
+ }
+
+ // ==================== Conditions ====================
+
+ private void generateCondition(final StringBuilder sb,
+ final LALScriptModel.Condition cond) {
+ if (cond instanceof LALScriptModel.ComparisonCondition) {
+ final LALScriptModel.ComparisonCondition cc =
+ (LALScriptModel.ComparisonCondition) cond;
+ switch (cc.getOp()) {
+ case EQ:
+ sb.append("java.util.Objects.equals(");
+ generateValueAccessObj(sb, cc.getLeft(), cc.getLeftCast());
+ sb.append(", ");
+ generateConditionValue(sb, cc.getRight());
+ sb.append(")");
+ break;
+ case NEQ:
+ sb.append("!java.util.Objects.equals(");
+ generateValueAccessObj(sb, cc.getLeft(), cc.getLeftCast());
+ sb.append(", ");
+ generateConditionValue(sb, cc.getRight());
+ sb.append(")");
+ break;
+ case GT:
+ sb.append("toLong(");
+ generateValueAccessObj(sb, cc.getLeft(), null);
+ sb.append(") > ");
+ generateConditionValueNumeric(sb, cc.getRight());
+ break;
+ case LT:
+ sb.append("toLong(");
+ generateValueAccessObj(sb, cc.getLeft(), null);
+ sb.append(") < ");
+ generateConditionValueNumeric(sb, cc.getRight());
+ break;
+ case GTE:
+ sb.append("toLong(");
+ generateValueAccessObj(sb, cc.getLeft(), null);
+ sb.append(") >= ");
+ generateConditionValueNumeric(sb, cc.getRight());
+ break;
+ case LTE:
+ sb.append("toLong(");
+ generateValueAccessObj(sb, cc.getLeft(), null);
+ sb.append(") <= ");
+ generateConditionValueNumeric(sb, cc.getRight());
+ break;
+ default:
+ break;
+ }
+ } else if (cond instanceof LALScriptModel.LogicalCondition) {
+ final LALScriptModel.LogicalCondition lc =
+ (LALScriptModel.LogicalCondition) cond;
+ sb.append("(");
+ generateCondition(sb, lc.getLeft());
+ sb.append(lc.getOp() == LALScriptModel.LogicalOp.AND
+ ? " && " : " || ");
+ generateCondition(sb, lc.getRight());
+ sb.append(")");
+ } else if (cond instanceof LALScriptModel.NotCondition) {
+ sb.append("!(");
+ generateCondition(sb,
+ ((LALScriptModel.NotCondition) cond).getInner());
+ sb.append(")");
+ } else if (cond instanceof LALScriptModel.ExprCondition) {
+ sb.append("isTruthy(");
+ generateValueAccessObj(sb,
+ ((LALScriptModel.ExprCondition) cond).getExpr(),
+ ((LALScriptModel.ExprCondition) cond).getCastType());
+ sb.append(")");
+ }
+ }
+
+ private void generateConditionValue(final StringBuilder sb,
+ final LALScriptModel.ConditionValue cv) {
+ if (cv instanceof LALScriptModel.StringConditionValue) {
+ sb.append('"')
+ .append(escapeJava(
+ ((LALScriptModel.StringConditionValue) cv).getValue()))
+ .append('"');
+ } else if (cv instanceof LALScriptModel.NumberConditionValue) {
+ final double val =
+ ((LALScriptModel.NumberConditionValue) cv).getValue();
+ sb.append("Long.valueOf(").append((long) val).append("L)");
+ } else if (cv instanceof LALScriptModel.BoolConditionValue) {
+ sb.append("Boolean.valueOf(")
+ .append(((LALScriptModel.BoolConditionValue) cv).isValue())
+ .append(")");
+ } else if (cv instanceof LALScriptModel.NullConditionValue) {
+ sb.append("null");
+ } else if (cv instanceof LALScriptModel.ValueAccessConditionValue) {
+ generateValueAccessObj(sb,
+ ((LALScriptModel.ValueAccessConditionValue) cv).getValue(),
+ null);
+ }
+ }
+
+ private void generateConditionValueNumeric(
+ final StringBuilder sb,
+ final LALScriptModel.ConditionValue cv) {
+ if (cv instanceof LALScriptModel.NumberConditionValue) {
+ sb.append((long) ((LALScriptModel.NumberConditionValue) cv)
+ .getValue()).append("L");
+ } else if (cv instanceof LALScriptModel.ValueAccessConditionValue) {
+ sb.append("toLong(");
+ generateValueAccessObj(sb,
+ ((LALScriptModel.ValueAccessConditionValue) cv).getValue(),
+ null);
+ sb.append(")");
+ } else {
+ sb.append("0L");
+ }
+ }
+
+ // ==================== Value access ====================
+
+ private void generateCastedValueAccess(final StringBuilder sb,
+ final LALScriptModel.ValueAccess value,
+ final String castType) {
+ if ("String".equals(castType)) {
+ sb.append("String.valueOf(");
+ generateValueAccess(sb, value);
+ sb.append(")");
+ } else if ("Long".equals(castType)) {
+ sb.append("toLong(");
+ generateValueAccess(sb, value);
+ sb.append(")");
+ } else if ("Integer".equals(castType)) {
+ sb.append("toInt(");
+ generateValueAccess(sb, value);
+ sb.append(")");
+ } else if ("Boolean".equals(castType)) {
+ sb.append("toBool(");
+ generateValueAccess(sb, value);
+ sb.append(")");
+ } else {
+ generateValueAccess(sb, value);
+ }
+ }
+
+ private void generateValueAccessObj(final StringBuilder sb,
+ final LALScriptModel.ValueAccess value,
+ final String castType) {
+ if ("String".equals(castType)) {
+ sb.append("String.valueOf(");
+ generateValueAccess(sb, value);
+ sb.append(")");
+ } else {
+ generateValueAccess(sb, value);
+ }
+ }
+
+ private void generateValueAccess(final StringBuilder sb,
+ final LALScriptModel.ValueAccess value) {
+ String current;
+ if (value.isParsedRef()) {
+ current = "binding.parsed()";
+ } else if (value.isLogRef()) {
+ current = "binding.log()";
+ } else {
+ final List<LALScriptModel.ValueAccessSegment> segs = value.getChain();
+ if (segs.isEmpty()) {
+ sb.append("null");
+ return;
+ }
+ current = "binding.parsed()";
+ }
+
+ final List<LALScriptModel.ValueAccessSegment> chain = value.getChain();
+ if (chain.isEmpty()) {
+ sb.append(current);
+ return;
+ }
+
+ for (int i = 0; i < chain.size(); i++) {
+ final LALScriptModel.ValueAccessSegment seg = chain.get(i);
+ if (seg instanceof LALScriptModel.FieldSegment) {
+ final String name =
+ ((LALScriptModel.FieldSegment) seg).getName();
+ current = "getAt(" + current + ", \""
+ + escapeJava(name) + "\")";
+ } else if (seg instanceof LALScriptModel.MethodSegment) {
+ final LALScriptModel.MethodSegment ms =
+ (LALScriptModel.MethodSegment) seg;
+ current = current + "." + ms.getName() + "()";
+ }
+ }
+ sb.append(current);
+ }
+
+ // ==================== Utilities ====================
+
+ private static String escapeJava(final String s) {
+ return s.replace("\\", "\\\\")
+ .replace("\"", "\\\"")
+ .replace("\n", "\\n")
+ .replace("\r", "\\r")
+ .replace("\t", "\\t");
+ }
+
+ /**
+ * Generates the Java source body of the execute method for
+ * debugging/testing.
+ */
+ public String generateSource(final String dsl) {
+ final LALScriptModel model = LALScriptParser.parse(dsl);
+ final int[] counter = {0};
+ return generateExecuteMethod(model, counter);
+ }
+}
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptModel.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptModel.java
new file mode 100644
index 0000000000..cc3d1d3df0
--- /dev/null
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptModel.java
@@ -0,0 +1,459 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.log.analyzer.compiler;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import lombok.Getter;
+
+/**
+ * Immutable AST model for LAL (Log Analysis Language) scripts.
+ *
+ * <p>Represents parsed scripts like:
+ * <pre>
+ * filter {
+ * json {}
+ * extractor { service parsed.service as String }
+ * sink { sampler { rateLimit("id") { rpm 6000 } } }
+ * }
+ * </pre>
+ */
+public final class LALScriptModel {
+
+ @Getter
+ private final List<FilterStatement> statements;
+
+ public LALScriptModel(final List<FilterStatement> statements) {
+ this.statements = Collections.unmodifiableList(statements);
+ }
+
+ // ==================== Filter statements ====================
+
+ public interface FilterStatement {
+ }
+
+ // ==================== Parser blocks ====================
+
+ @Getter
+ public static final class TextParser implements FilterStatement {
+ private final String regexpPattern;
+ private final boolean abortOnFailure;
+
+ public TextParser(final String regexpPattern, final boolean abortOnFailure) {
+ this.regexpPattern = regexpPattern;
+ this.abortOnFailure = abortOnFailure;
+ }
+ }
+
+ @Getter
+ public static final class JsonParser implements FilterStatement {
+ private final boolean abortOnFailure;
+
+ public JsonParser(final boolean abortOnFailure) {
+ this.abortOnFailure = abortOnFailure;
+ }
+ }
+
+ @Getter
+ public static final class YamlParser implements FilterStatement {
+ private final boolean abortOnFailure;
+
+ public YamlParser(final boolean abortOnFailure) {
+ this.abortOnFailure = abortOnFailure;
+ }
+ }
+
+ public static final class AbortStatement implements FilterStatement {
+ }
+
+ // ==================== Extractor block ====================
+
+ @Getter
+ public static final class ExtractorBlock implements FilterStatement {
+ private final List<ExtractorStatement> statements;
+
+ public ExtractorBlock(final List<ExtractorStatement> statements) {
+ this.statements = Collections.unmodifiableList(statements);
+ }
+ }
+
+ public interface ExtractorStatement {
+ }
+
+ @Getter
+ public static final class FieldAssignment implements ExtractorStatement, FilterStatement {
+ private final FieldType fieldType;
+ private final ValueAccess value;
+ private final String castType;
+ private final String formatPattern;
+
+ public FieldAssignment(final FieldType fieldType,
+ final ValueAccess value,
+ final String castType,
+ final String formatPattern) {
+ this.fieldType = fieldType;
+ this.value = value;
+ this.castType = castType;
+ this.formatPattern = formatPattern;
+ }
+ }
+
+ public enum FieldType {
+ SERVICE, INSTANCE, ENDPOINT, LAYER,
+ TRACE_ID, SEGMENT_ID, SPAN_ID, TIMESTAMP
+ }
+
+ @Getter
+ public static final class TagAssignment implements ExtractorStatement, FilterStatement {
+ private final Map<String, TagValue> tags;
+
+ public TagAssignment(final Map<String, TagValue> tags) {
+ this.tags = Collections.unmodifiableMap(tags);
+ }
+ }
+
+ @Getter
+ public static final class TagValue {
+ private final ValueAccess value;
+ private final String castType;
+
+ public TagValue(final ValueAccess value, final String castType) {
+ this.value = value;
+ this.castType = castType;
+ }
+ }
+
+ @Getter
+ public static final class MetricsBlock implements ExtractorStatement, FilterStatement {
+ private final String name;
+ private final ValueAccess timestampValue;
+ private final String timestampCast;
+ private final Map<String, TagValue> labels;
+ private final ValueAccess value;
+ private final String valueCast;
+
+ public MetricsBlock(final String name,
+ final ValueAccess timestampValue,
+ final String timestampCast,
+ final Map<String, TagValue> labels,
+ final ValueAccess value,
+ final String valueCast) {
+ this.name = name;
+ this.timestampValue = timestampValue;
+ this.timestampCast = timestampCast;
+ this.labels = labels != null ? Collections.unmodifiableMap(labels) : Collections.emptyMap();
+ this.value = value;
+ this.valueCast = valueCast;
+ }
+ }
+
+ @Getter
+ public static final class SlowSqlBlock implements ExtractorStatement, FilterStatement {
+ private final ValueAccess id;
+ private final String idCast;
+ private final ValueAccess statement;
+ private final String statementCast;
+ private final ValueAccess latency;
+ private final String latencyCast;
+
+ public SlowSqlBlock(final ValueAccess id, final String idCast,
+ final ValueAccess statement, final String statementCast,
+ final ValueAccess latency, final String latencyCast) {
+ this.id = id;
+ this.idCast = idCast;
+ this.statement = statement;
+ this.statementCast = statementCast;
+ this.latency = latency;
+ this.latencyCast = latencyCast;
+ }
+ }
+
+ @Getter
+ public static final class SampledTraceBlock implements ExtractorStatement, FilterStatement {
+ private final List<SampledTraceStatement> statements;
+
+ public SampledTraceBlock(final List<SampledTraceStatement> statements) {
+ this.statements = Collections.unmodifiableList(statements);
+ }
+ }
+
+ public interface SampledTraceStatement {
+ }
+
+ @Getter
+ public static final class SampledTraceField implements SampledTraceStatement {
+ private final SampledTraceFieldType fieldType;
+ private final ValueAccess value;
+ private final String castType;
+
+ public SampledTraceField(final SampledTraceFieldType fieldType,
+ final ValueAccess value,
+ final String castType) {
+ this.fieldType = fieldType;
+ this.value = value;
+ this.castType = castType;
+ }
+ }
+
+ public enum SampledTraceFieldType {
+ LATENCY, URI, REASON, PROCESS_ID, DEST_PROCESS_ID,
+ DETECT_POINT, COMPONENT_ID, REPORT_SERVICE
+ }
+
+ // ==================== Sink block ====================
+
+ @Getter
+ public static final class SinkBlock implements FilterStatement {
+ private final List<SinkStatement> statements;
+
+ public SinkBlock(final List<SinkStatement> statements) {
+ this.statements = Collections.unmodifiableList(statements);
+ }
+ }
+
+ public interface SinkStatement {
+ }
+
+ @Getter
+ public static final class SamplerBlock implements SinkStatement, FilterStatement {
+ private final List<SamplerContent> contents;
+
+ public SamplerBlock(final List<SamplerContent> contents) {
+ this.contents = Collections.unmodifiableList(contents);
+ }
+ }
+
+ public interface SamplerContent {
+ }
+
+ @Getter
+ public static final class RateLimitBlock implements SamplerContent {
+ private final String id;
+ private final long rpm;
+
+ public RateLimitBlock(final String id, final long rpm) {
+ this.id = id;
+ this.rpm = rpm;
+ }
+ }
+
+ public static final class EnforcerStatement implements SinkStatement, FilterStatement {
+ }
+
+ public static final class DropperStatement implements SinkStatement, FilterStatement {
+ }
+
+ // ==================== Control flow ====================
+
+ @Getter
+ public static final class IfBlock implements FilterStatement, ExtractorStatement,
+ SinkStatement, SampledTraceStatement, SamplerContent {
+ private final Condition condition;
+ private final List<FilterStatement> thenBranch;
+ private final List<FilterStatement> elseBranch;
+
+ public IfBlock(final Condition condition,
+ final List<FilterStatement> thenBranch,
+ final List<FilterStatement> elseBranch) {
+ this.condition = condition;
+ this.thenBranch = Collections.unmodifiableList(thenBranch);
+ this.elseBranch = elseBranch != null
+ ? Collections.unmodifiableList(elseBranch) : Collections.emptyList();
+ }
+ }
+
+ // ==================== Conditions ====================
+
+ public interface Condition {
+ }
+
+ @Getter
+ public static final class ComparisonCondition implements Condition {
+ private final ValueAccess left;
+ private final String leftCast;
+ private final CompareOp op;
+ private final ConditionValue right;
+
+ public ComparisonCondition(final ValueAccess left,
+ final String leftCast,
+ final CompareOp op,
+ final ConditionValue right) {
+ this.left = left;
+ this.leftCast = leftCast;
+ this.op = op;
+ this.right = right;
+ }
+ }
+
+ @Getter
+ public static final class LogicalCondition implements Condition {
+ private final Condition left;
+ private final LogicalOp op;
+ private final Condition right;
+
+ public LogicalCondition(final Condition left, final LogicalOp op, final Condition right) {
+ this.left = left;
+ this.op = op;
+ this.right = right;
+ }
+ }
+
+ @Getter
+ public static final class NotCondition implements Condition {
+ private final Condition inner;
+
+ public NotCondition(final Condition inner) {
+ this.inner = inner;
+ }
+ }
+
+ @Getter
+ public static final class ExprCondition implements Condition {
+ private final ValueAccess expr;
+ private final String castType;
+
+ public ExprCondition(final ValueAccess expr, final String castType) {
+ this.expr = expr;
+ this.castType = castType;
+ }
+ }
+
+ // ==================== Value access ====================
+
+ @Getter
+ public static final class ValueAccess {
+ private final List<String> segments;
+ private final boolean parsedRef;
+ private final boolean logRef;
+ private final List<ValueAccessSegment> chain;
+
+ public ValueAccess(final List<String> segments,
+ final boolean parsedRef,
+ final boolean logRef,
+ final List<ValueAccessSegment> chain) {
+ this.segments = Collections.unmodifiableList(segments);
+ this.parsedRef = parsedRef;
+ this.logRef = logRef;
+ this.chain = chain != null
+ ? Collections.unmodifiableList(chain) : Collections.emptyList();
+ }
+
+ public String toPathString() {
+ return String.join(".", segments);
+ }
+ }
+
+ public interface ValueAccessSegment {
+ }
+
+ @Getter
+ public static final class FieldSegment implements ValueAccessSegment {
+ private final String name;
+ private final boolean safeNav;
+
+ public FieldSegment(final String name, final boolean safeNav) {
+ this.name = name;
+ this.safeNav = safeNav;
+ }
+ }
+
+ @Getter
+ public static final class MethodSegment implements ValueAccessSegment {
+ private final String name;
+ private final List<String> arguments;
+ private final boolean safeNav;
+
+ public MethodSegment(final String name, final List<String> arguments, final boolean safeNav) {
+ this.name = name;
+ this.arguments = Collections.unmodifiableList(arguments);
+ this.safeNav = safeNav;
+ }
+ }
+
+ // ==================== Condition values ====================
+
+ public interface ConditionValue {
+ }
+
+ @Getter
+ public static final class StringConditionValue implements ConditionValue {
+ private final String value;
+
+ public StringConditionValue(final String value) {
+ this.value = value;
+ }
+ }
+
+ @Getter
+ public static final class NumberConditionValue implements ConditionValue {
+ private final double value;
+
+ public NumberConditionValue(final double value) {
+ this.value = value;
+ }
+ }
+
+ @Getter
+ public static final class BoolConditionValue implements ConditionValue {
+ private final boolean value;
+
+ public BoolConditionValue(final boolean value) {
+ this.value = value;
+ }
+ }
+
+ public static final class NullConditionValue implements ConditionValue {
+ }
+
+ @Getter
+ public static final class ValueAccessConditionValue implements ConditionValue {
+ private final ValueAccess value;
+ private final String castType;
+
+ public ValueAccessConditionValue(final ValueAccess value, final String castType) {
+ this.value = value;
+ this.castType = castType;
+ }
+ }
+
+ @Getter
+ public static final class FunctionCallConditionValue implements ConditionValue {
+ private final String functionName;
+ private final List<String> arguments;
+
+ public FunctionCallConditionValue(final String functionName, final List<String> arguments) {
+ this.functionName = functionName;
+ this.arguments = Collections.unmodifiableList(arguments);
+ }
+ }
+
+ // ==================== Enums ====================
+
+ public enum CompareOp {
+ EQ, NEQ, GT, LT, GTE, LTE
+ }
+
+ public enum LogicalOp {
+ AND, OR
+ }
+
+ private LALScriptModel() {
+ this.statements = Collections.emptyList();
+ }
+}
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptParser.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptParser.java
new file mode 100644
index 0000000000..128f325573
--- /dev/null
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptParser.java
@@ -0,0 +1,687 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.log.analyzer.compiler;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.antlr.v4.runtime.BaseErrorListener;
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.antlr.v4.runtime.RecognitionException;
+import org.antlr.v4.runtime.Recognizer;
+import org.apache.skywalking.lal.rt.grammar.LALLexer;
+import org.apache.skywalking.lal.rt.grammar.LALParser;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.AbortStatement;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.CompareOp;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.ComparisonCondition;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.Condition;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.DropperStatement;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.EnforcerStatement;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.ExprCondition;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.ExtractorBlock;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.ExtractorStatement;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.FieldAssignment;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.FieldSegment;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.FieldType;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.FilterStatement;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.IfBlock;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.JsonParser;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.LogicalCondition;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.LogicalOp;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.MetricsBlock;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.NotCondition;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.NullConditionValue;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.NumberConditionValue;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.RateLimitBlock;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.SamplerBlock;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.SamplerContent;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.SinkBlock;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.SinkStatement;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.SlowSqlBlock;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.StringConditionValue;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.TagAssignment;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.TagValue;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.TextParser;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.ValueAccess;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.ValueAccessConditionValue;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.ValueAccessSegment;
+import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.YamlParser;
+
+/**
+ * Facade: parses LAL DSL script strings into {@link LALScriptModel}.
+ *
+ * <pre>
+ * LALScriptModel model = LALScriptParser.parse(
+ * "filter { json {} extractor { service parsed.service as String } sink {} }");
+ * </pre>
+ */
+public final class LALScriptParser {
+
+ private LALScriptParser() {
+ }
+
+ public static LALScriptModel parse(final String dsl) {
+ final LALLexer lexer = new LALLexer(CharStreams.fromString(dsl));
+ final CommonTokenStream tokens = new CommonTokenStream(lexer);
+ final LALParser parser = new LALParser(tokens);
+
+ final List<String> errors = new ArrayList<>();
+ parser.removeErrorListeners();
+ parser.addErrorListener(new BaseErrorListener() {
+ @Override
+ public void syntaxError(final Recognizer<?, ?> recognizer,
+ final Object offendingSymbol,
+ final int line,
+ final int charPositionInLine,
+ final String msg,
+ final RecognitionException e) {
+ errors.add(line + ":" + charPositionInLine + " " + msg);
+ }
+ });
+
+ final LALParser.RootContext tree = parser.root();
+ if (!errors.isEmpty()) {
+ throw new IllegalArgumentException(
+ "LAL script parsing failed: " + String.join("; ", errors)
+ + " in script: " + truncate(dsl, 200));
+ }
+
+ final List<FilterStatement> stmts = visitFilterContent(
+ tree.filterBlock().filterContent());
+ return new LALScriptModel(stmts);
+ }
+
+ // ==================== Filter content ====================
+
+ private static List<FilterStatement> visitFilterContent(
+ final LALParser.FilterContentContext ctx) {
+ final List<FilterStatement> stmts = new ArrayList<>();
+ for (final LALParser.FilterStatementContext fsc : ctx.filterStatement()) {
+ stmts.add(visitFilterStatement(fsc));
+ }
+ return stmts;
+ }
+
+ private static FilterStatement visitFilterStatement(
+ final LALParser.FilterStatementContext ctx) {
+ if (ctx.parserBlock() != null) {
+ return visitParserBlock(ctx.parserBlock());
+ }
+ if (ctx.extractorBlock() != null) {
+ return visitExtractorBlock(ctx.extractorBlock());
+ }
+ if (ctx.sinkBlock() != null) {
+ return visitSinkBlock(ctx.sinkBlock());
+ }
+ if (ctx.abortBlock() != null) {
+ return new AbortStatement();
+ }
+ // ifStatement
+ return visitIfStatement(ctx.ifStatement());
+ }
+
+ // ==================== Parser blocks ====================
+
+ private static FilterStatement visitParserBlock(final LALParser.ParserBlockContext ctx) {
+ if (ctx.textBlock() != null) {
+ String pattern = null;
+ boolean abortOnFail = false;
+ if (ctx.textBlock().textContent() != null) {
+ for (final LALParser.RegexpStatementContext regCtx :
+ ctx.textBlock().textContent().regexpStatement()) {
+ pattern = stripQuotes(regCtx.regexpPattern().getText());
+ }
+ for (final LALParser.AbortOnFailureStatementContext abfCtx :
+ ctx.textBlock().textContent().abortOnFailureStatement()) {
+ abortOnFail = "true".equals(abfCtx.boolValue().getText());
+ }
+ }
+ return new TextParser(pattern, abortOnFail);
+ }
+ if (ctx.jsonBlock() != null) {
+ boolean abortOnFail = false;
+ if (ctx.jsonBlock().jsonContent() != null
+ && ctx.jsonBlock().jsonContent().abortOnFailureStatement() != null) {
+ abortOnFail = "true".equals(
+ ctx.jsonBlock().jsonContent().abortOnFailureStatement()
+ .boolValue().getText());
+ }
+ return new JsonParser(abortOnFail);
+ }
+ // yaml
+ boolean abortOnFail = false;
+ if (ctx.yamlBlock().yamlContent() != null
+ && ctx.yamlBlock().yamlContent().abortOnFailureStatement() != null) {
+ abortOnFail = "true".equals(
+ ctx.yamlBlock().yamlContent().abortOnFailureStatement()
+ .boolValue().getText());
+ }
+ return new YamlParser(abortOnFail);
+ }
+
+ // ==================== Extractor block ====================
+
+ private static ExtractorBlock visitExtractorBlock(
+ final LALParser.ExtractorBlockContext ctx) {
+ final List<ExtractorStatement> stmts = new ArrayList<>();
+ for (final LALParser.ExtractorStatementContext esc : ctx.extractorContent().extractorStatement()) {
+ stmts.add(visitExtractorStatement(esc));
+ }
+ return new ExtractorBlock(stmts);
+ }
+
+ private static ExtractorStatement visitExtractorStatement(
+ final LALParser.ExtractorStatementContext ctx) {
+ if (ctx.serviceStatement() != null) {
+ return visitFieldAssignment(FieldType.SERVICE, ctx.serviceStatement().valueAccess(),
+ ctx.serviceStatement().typeCast());
+ }
+ if (ctx.instanceStatement() != null) {
+ return visitFieldAssignment(FieldType.INSTANCE, ctx.instanceStatement().valueAccess(),
+ ctx.instanceStatement().typeCast());
+ }
+ if (ctx.endpointStatement() != null) {
+ return visitFieldAssignment(FieldType.ENDPOINT, ctx.endpointStatement().valueAccess(),
+ ctx.endpointStatement().typeCast());
+ }
+ if (ctx.layerStatement() != null) {
+ return visitFieldAssignment(FieldType.LAYER, ctx.layerStatement().valueAccess(),
+ ctx.layerStatement().typeCast());
+ }
+ if (ctx.traceIdStatement() != null) {
+ return visitFieldAssignment(FieldType.TRACE_ID, ctx.traceIdStatement().valueAccess(),
+ ctx.traceIdStatement().typeCast());
+ }
+ if (ctx.timestampStatement() != null) {
+ final ValueAccess va = visitValueAccess(ctx.timestampStatement().valueAccess());
+ final String cast = ctx.timestampStatement().typeCast() != null
+ ? extractCastType(ctx.timestampStatement().typeCast()) : null;
+ String format = null;
+ if (ctx.timestampStatement().STRING() != null) {
+ format = stripQuotes(ctx.timestampStatement().STRING().getText());
+ }
+ return new FieldAssignment(FieldType.TIMESTAMP, va, cast, format);
+ }
+ if (ctx.tagStatement() != null) {
+ return visitTagStatement(ctx.tagStatement());
+ }
+ if (ctx.metricsBlock() != null) {
+ return visitMetricsBlock(ctx.metricsBlock());
+ }
+ if (ctx.slowSqlBlock() != null) {
+ return visitSlowSqlBlock(ctx.slowSqlBlock());
+ }
+ if (ctx.sampledTraceBlock() != null) {
+ return visitSampledTraceBlock(ctx.sampledTraceBlock());
+ }
+ // ifStatement
+ return (ExtractorStatement) visitIfStatement(ctx.ifStatement());
+ }
+
+ private static FieldAssignment visitFieldAssignment(
+ final FieldType type,
+ final LALParser.ValueAccessContext vaCtx,
+ final LALParser.TypeCastContext tcCtx) {
+ final ValueAccess va = visitValueAccess(vaCtx);
+ final String cast = tcCtx != null ? extractCastType(tcCtx) : null;
+ return new FieldAssignment(type, va, cast, null);
+ }
+
+ // ==================== Tag statement ====================
+
+ private static TagAssignment visitTagStatement(final LALParser.TagStatementContext ctx) {
+ final Map<String, TagValue> tags = new LinkedHashMap<>();
+ if (ctx.tagMap() != null) {
+ for (int i = 0; i < ctx.tagMap().anyIdentifier().size(); i++) {
+ final String key = ctx.tagMap().anyIdentifier(i).getText();
+ final ValueAccess va = visitValueAccess(ctx.tagMap().valueAccess(i));
+ final String cast = ctx.tagMap().typeCast(i) != null
+ ? extractCastType(ctx.tagMap().typeCast(i)) : null;
+ tags.put(key, new TagValue(va, cast));
+ }
+ } else if (ctx.STRING() != null) {
+ final String key = stripQuotes(ctx.STRING().getText());
+ final ValueAccess va = visitValueAccess(ctx.valueAccess());
+ final String cast = ctx.typeCast() != null ? extractCastType(ctx.typeCast()) : null;
+ tags.put(key, new TagValue(va, cast));
+ }
+ return new TagAssignment(tags);
+ }
+
+ // ==================== Metrics block ====================
+
+ private static MetricsBlock visitMetricsBlock(final LALParser.MetricsBlockContext ctx) {
+ String name = null;
+ ValueAccess timestampValue = null;
+ String timestampCast = null;
+ final Map<String, TagValue> labels = new LinkedHashMap<>();
+ ValueAccess value = null;
+ String valueCast = null;
+
+ for (final LALParser.MetricsStatementContext msc : ctx.metricsContent().metricsStatement()) {
+ if (msc.metricsNameStatement() != null) {
+ name = resolveValueAsString(msc.metricsNameStatement().valueAccess());
+ }
+ if (msc.metricsTimestampStatement() != null) {
+ timestampValue = visitValueAccess(msc.metricsTimestampStatement().valueAccess());
+ timestampCast = msc.metricsTimestampStatement().typeCast() != null
+ ? extractCastType(msc.metricsTimestampStatement().typeCast()) : null;
+ }
+ if (msc.metricsLabelsStatement() != null) {
+ for (final LALParser.LabelEntryContext lec :
+ msc.metricsLabelsStatement().labelMap().labelEntry()) {
+ final String key = lec.anyIdentifier().getText();
+ final ValueAccess va = visitValueAccess(lec.valueAccess());
+ final String cast = lec.typeCast() != null
+ ? extractCastType(lec.typeCast()) : null;
+ labels.put(key, new TagValue(va, cast));
+ }
+ }
+ if (msc.metricsValueStatement() != null) {
+ value = visitValueAccess(msc.metricsValueStatement().valueAccess());
+ valueCast = msc.metricsValueStatement().typeCast() != null
+ ? extractCastType(msc.metricsValueStatement().typeCast()) : null;
+ }
+ }
+
+ return new MetricsBlock(name, timestampValue, timestampCast, labels, value, valueCast);
+ }
+
+ // ==================== Slow SQL block ====================
+
+ private static SlowSqlBlock visitSlowSqlBlock(final LALParser.SlowSqlBlockContext ctx) {
+ ValueAccess id = null;
+ String idCast = null;
+ ValueAccess statement = null;
+ String statementCast = null;
+ ValueAccess latency = null;
+ String latencyCast = null;
+
+ for (final LALParser.SlowSqlStatementContext ssc :
+ ctx.slowSqlContent().slowSqlStatement()) {
+ if (ssc.slowSqlIdStatement() != null) {
+ id = visitValueAccess(ssc.slowSqlIdStatement().valueAccess());
+ idCast = ssc.slowSqlIdStatement().typeCast() != null
+ ? extractCastType(ssc.slowSqlIdStatement().typeCast()) : null;
+ }
+ if (ssc.slowSqlStatementStatement() != null) {
+ statement = visitValueAccess(ssc.slowSqlStatementStatement().valueAccess());
+ statementCast = ssc.slowSqlStatementStatement().typeCast() != null
+ ? extractCastType(ssc.slowSqlStatementStatement().typeCast()) : null;
+ }
+ if (ssc.slowSqlLatencyStatement() != null) {
+ latency = visitValueAccess(ssc.slowSqlLatencyStatement().valueAccess());
+ latencyCast = ssc.slowSqlLatencyStatement().typeCast() != null
+ ? extractCastType(ssc.slowSqlLatencyStatement().typeCast()) : null;
+ }
+ }
+
+ return new SlowSqlBlock(id, idCast, statement, statementCast, latency, latencyCast);
+ }
+
+ // ==================== Sampled trace block ====================
+
+ private static LALScriptModel.SampledTraceBlock visitSampledTraceBlock(
+ final LALParser.SampledTraceBlockContext ctx) {
+ final List<LALScriptModel.SampledTraceStatement> stmts = new ArrayList<>();
+ for (final LALParser.SampledTraceStatementContext stc :
+ ctx.sampledTraceContent().sampledTraceStatement()) {
+ if (stc.ifStatement() != null) {
+ stmts.add((LALScriptModel.SampledTraceStatement) visitIfStatement(
+ stc.ifStatement()));
+ } else {
+ stmts.add(visitSampledTraceField(stc));
+ }
+ }
+ return new LALScriptModel.SampledTraceBlock(stmts);
+ }
+
+ private static LALScriptModel.SampledTraceField visitSampledTraceField(
+ final LALParser.SampledTraceStatementContext ctx) {
+ if (ctx.sampledTraceLatencyStatement() != null) {
+ return makeSampledField(LALScriptModel.SampledTraceFieldType.LATENCY,
+ ctx.sampledTraceLatencyStatement().valueAccess(),
+ ctx.sampledTraceLatencyStatement().typeCast());
+ }
+ if (ctx.sampledTraceUriStatement() != null) {
+ return makeSampledField(LALScriptModel.SampledTraceFieldType.URI,
+ ctx.sampledTraceUriStatement().valueAccess(),
+ ctx.sampledTraceUriStatement().typeCast());
+ }
+ if (ctx.sampledTraceReasonStatement() != null) {
+ return makeSampledField(LALScriptModel.SampledTraceFieldType.REASON,
+ ctx.sampledTraceReasonStatement().valueAccess(),
+ ctx.sampledTraceReasonStatement().typeCast());
+ }
+ if (ctx.sampledTraceProcessIdStatement() != null) {
+ return makeSampledField(LALScriptModel.SampledTraceFieldType.PROCESS_ID,
+ ctx.sampledTraceProcessIdStatement().valueAccess(),
+ ctx.sampledTraceProcessIdStatement().typeCast());
+ }
+ if (ctx.sampledTraceDestProcessIdStatement() != null) {
+ return makeSampledField(LALScriptModel.SampledTraceFieldType.DEST_PROCESS_ID,
+ ctx.sampledTraceDestProcessIdStatement().valueAccess(),
+ ctx.sampledTraceDestProcessIdStatement().typeCast());
+ }
+ if (ctx.sampledTraceDetectPointStatement() != null) {
+ return makeSampledField(LALScriptModel.SampledTraceFieldType.DETECT_POINT,
+ ctx.sampledTraceDetectPointStatement().valueAccess(),
+ ctx.sampledTraceDetectPointStatement().typeCast());
+ }
+ if (ctx.sampledTraceComponentIdStatement() != null) {
+ return makeSampledField(LALScriptModel.SampledTraceFieldType.COMPONENT_ID,
+ ctx.sampledTraceComponentIdStatement().valueAccess(),
+ ctx.sampledTraceComponentIdStatement().typeCast());
+ }
+ // reportService
+ return makeSampledField(LALScriptModel.SampledTraceFieldType.REPORT_SERVICE,
+ ctx.reportServiceStatement().valueAccess(),
+ ctx.reportServiceStatement().typeCast());
+ }
+
+ private static LALScriptModel.SampledTraceField makeSampledField(
+ final LALScriptModel.SampledTraceFieldType type,
+ final LALParser.ValueAccessContext vaCtx,
+ final LALParser.TypeCastContext tcCtx) {
+ return new LALScriptModel.SampledTraceField(
+ type, visitValueAccess(vaCtx),
+ tcCtx != null ? extractCastType(tcCtx) : null);
+ }
+
+ // ==================== Sink block ====================
+
+ private static SinkBlock visitSinkBlock(final LALParser.SinkBlockContext ctx) {
+ final List<SinkStatement> stmts = new ArrayList<>();
+ for (final LALParser.SinkStatementContext ssc : ctx.sinkContent().sinkStatement()) {
+ if (ssc.samplerBlock() != null) {
+ stmts.add(visitSamplerBlock(ssc.samplerBlock()));
+ } else if (ssc.enforcerStatement() != null) {
+ stmts.add(new EnforcerStatement());
+ } else if (ssc.dropperStatement() != null) {
+ stmts.add(new DropperStatement());
+ } else {
+ stmts.add((SinkStatement) visitIfStatement(ssc.ifStatement()));
+ }
+ }
+ return new SinkBlock(stmts);
+ }
+
+ private static SamplerBlock visitSamplerBlock(final LALParser.SamplerBlockContext ctx) {
+ final List<SamplerContent> contents = new ArrayList<>();
+ for (final LALParser.RateLimitBlockContext rlc : ctx.samplerContent().rateLimitBlock()) {
+ final String id = stripQuotes(rlc.rateLimitId().getText());
+ final long rpm = Long.parseLong(rlc.rateLimitContent().NUMBER().getText());
+ contents.add(new RateLimitBlock(id, rpm));
+ }
+ for (final LALParser.IfStatementContext isc : ctx.samplerContent().ifStatement()) {
+ contents.add((SamplerContent) visitIfStatement(isc));
+ }
+ return new SamplerBlock(contents);
+ }
+
+ // ==================== If statement ====================
+
+ private static IfBlock visitIfStatement(final LALParser.IfStatementContext ctx) {
+ final Condition condition = visitCondition(ctx.condition(0));
+ final List<FilterStatement> thenBranch = visitIfBody(ctx.ifBody(0));
+
+ List<FilterStatement> elseBranch = null;
+ // Handle else-if and else branches
+ final int bodyCount = ctx.ifBody().size();
+ if (bodyCount > 1) {
+ elseBranch = visitIfBody(ctx.ifBody(bodyCount - 1));
+ }
+
+ return new IfBlock(condition, thenBranch, elseBranch);
+ }
+
+ private static List<FilterStatement> visitIfBody(final LALParser.IfBodyContext ctx) {
+ final List<FilterStatement> stmts = new ArrayList<>();
+ for (final LALParser.FilterStatementContext fsc : ctx.filterStatement()) {
+ stmts.add(visitFilterStatement(fsc));
+ }
+ for (final LALParser.ExtractorStatementContext esc : ctx.extractorStatement()) {
+ stmts.add((FilterStatement) visitExtractorStatement(esc));
+ }
+ for (final LALParser.SinkStatementContext ssc : ctx.sinkStatement()) {
+ if (ssc.samplerBlock() != null) {
+ stmts.add((FilterStatement) visitSamplerBlock(ssc.samplerBlock()));
+ } else if (ssc.enforcerStatement() != null) {
+ stmts.add((FilterStatement) new EnforcerStatement());
+ } else if (ssc.dropperStatement() != null) {
+ stmts.add((FilterStatement) new DropperStatement());
+ }
+ }
+ return stmts;
+ }
+
+ // ==================== Conditions ====================
+
+ private static Condition visitCondition(final LALParser.ConditionContext ctx) {
+ if (ctx instanceof LALParser.CondAndContext) {
+ final LALParser.CondAndContext and = (LALParser.CondAndContext) ctx;
+ return new LogicalCondition(
+ visitCondition(and.condition(0)),
+ LogicalOp.AND,
+ visitCondition(and.condition(1)));
+ }
+ if (ctx instanceof LALParser.CondOrContext) {
+ final LALParser.CondOrContext or = (LALParser.CondOrContext) ctx;
+ return new LogicalCondition(
+ visitCondition(or.condition(0)),
+ LogicalOp.OR,
+ visitCondition(or.condition(1)));
+ }
+ if (ctx instanceof LALParser.CondNotContext) {
+ return new NotCondition(
+ visitCondition(((LALParser.CondNotContext) ctx).condition()));
+ }
+ if (ctx instanceof LALParser.CondParenContext) {
+ return visitCondition(((LALParser.CondParenContext) ctx).condition());
+ }
+ if (ctx instanceof LALParser.CondEqContext) {
+ final LALParser.CondEqContext eq = (LALParser.CondEqContext) ctx;
+ return makeComparison(eq.conditionExpr(0), CompareOp.EQ, eq.conditionExpr(1));
+ }
+ if (ctx instanceof LALParser.CondNeqContext) {
+ final LALParser.CondNeqContext neq = (LALParser.CondNeqContext) ctx;
+ return makeComparison(neq.conditionExpr(0), CompareOp.NEQ, neq.conditionExpr(1));
+ }
+ if (ctx instanceof LALParser.CondGtContext) {
+ final LALParser.CondGtContext gt = (LALParser.CondGtContext) ctx;
+ return makeComparison(gt.conditionExpr(0), CompareOp.GT, gt.conditionExpr(1));
+ }
+ if (ctx instanceof LALParser.CondLtContext) {
+ final LALParser.CondLtContext lt = (LALParser.CondLtContext) ctx;
+ return makeComparison(lt.conditionExpr(0), CompareOp.LT, lt.conditionExpr(1));
+ }
+ if (ctx instanceof LALParser.CondGteContext) {
+ final LALParser.CondGteContext gte = (LALParser.CondGteContext) ctx;
+ return makeComparison(gte.conditionExpr(0), CompareOp.GTE, gte.conditionExpr(1));
+ }
+ if (ctx instanceof LALParser.CondLteContext) {
+ final LALParser.CondLteContext lte = (LALParser.CondLteContext) ctx;
+ return makeComparison(lte.conditionExpr(0), CompareOp.LTE, lte.conditionExpr(1));
+ }
+ // condSingle
+ final LALParser.CondSingleContext single = (LALParser.CondSingleContext) ctx;
+ return visitConditionExprAsCondition(single.conditionExpr());
+ }
+
+ private static Condition makeComparison(
+ final LALParser.ConditionExprContext leftCtx,
+ final CompareOp op,
+ final LALParser.ConditionExprContext rightCtx) {
+ if (leftCtx instanceof LALParser.CondValueAccessContext) {
+ final LALParser.CondValueAccessContext lva =
+ (LALParser.CondValueAccessContext) leftCtx;
+ final ValueAccess left = visitValueAccess(lva.valueAccess());
+ final String leftCast = lva.typeCast() != null
+ ? extractCastType(lva.typeCast()) : null;
+ return new ComparisonCondition(left, leftCast, op,
+ visitConditionExprAsValue(rightCtx));
+ }
+ // For function calls and other forms, wrap as expression condition
+ return new ExprCondition(
+ new ValueAccess(List.of(leftCtx.getText()), false, false, List.of()), null);
+ }
+
+ private static LALScriptModel.ConditionValue visitConditionExprAsValue(
+ final LALParser.ConditionExprContext ctx) {
+ if (ctx instanceof LALParser.CondStringContext) {
+ return new StringConditionValue(
+ stripQuotes(((LALParser.CondStringContext) ctx).STRING().getText()));
+ }
+ if (ctx instanceof LALParser.CondNumberContext) {
+ return new NumberConditionValue(
+ Double.parseDouble(((LALParser.CondNumberContext) ctx).NUMBER().getText()));
+ }
+ if (ctx instanceof LALParser.CondNullContext) {
+ return new NullConditionValue();
+ }
+ if (ctx instanceof LALParser.CondValueAccessContext) {
+ final LALParser.CondValueAccessContext va =
+ (LALParser.CondValueAccessContext) ctx;
+ final String cast = va.typeCast() != null ? extractCastType(va.typeCast()) : null;
+ return new ValueAccessConditionValue(visitValueAccess(va.valueAccess()), cast);
+ }
+ // condBool, condFunctionCall
+ return new StringConditionValue(ctx.getText());
+ }
+
+ private static Condition visitConditionExprAsCondition(
+ final LALParser.ConditionExprContext ctx) {
+ if (ctx instanceof LALParser.CondValueAccessContext) {
+ final LALParser.CondValueAccessContext va =
+ (LALParser.CondValueAccessContext) ctx;
+ final String cast = va.typeCast() != null ? extractCastType(va.typeCast()) : null;
+ return new ExprCondition(visitValueAccess(va.valueAccess()), cast);
+ }
+ return new ExprCondition(
+ new ValueAccess(List.of(ctx.getText()), false, false, List.of()), null);
+ }
+
+ // ==================== Value access ====================
+
+ private static ValueAccess visitValueAccess(final LALParser.ValueAccessContext ctx) {
+ final List<String> segments = new ArrayList<>();
+ boolean parsedRef = false;
+ boolean logRef = false;
+
+ final LALParser.ValueAccessPrimaryContext primary = ctx.valueAccessPrimary();
+ if (primary instanceof LALParser.ValueParsedContext) {
+ parsedRef = true;
+ segments.add("parsed");
+ } else if (primary instanceof LALParser.ValueLogContext) {
+ logRef = true;
+ segments.add("log");
+ } else if (primary instanceof LALParser.ValueIdentifierContext) {
+ segments.add(((LALParser.ValueIdentifierContext) primary).IDENTIFIER().getText());
+ } else if (primary instanceof LALParser.ValueStringContext) {
+ segments.add(stripQuotes(
+ ((LALParser.ValueStringContext) primary).STRING().getText()));
+ } else if (primary instanceof LALParser.ValueNumberContext) {
+ segments.add(((LALParser.ValueNumberContext) primary).NUMBER().getText());
+ } else if (primary instanceof LALParser.ValueFunctionCallContext) {
+ final LALParser.FunctionInvocationContext fi =
+ ((LALParser.ValueFunctionCallContext) primary).functionInvocation();
+ segments.add(fi.getText());
+ } else {
+ segments.add(primary.getText());
+ }
+
+ final List<ValueAccessSegment> chain = new ArrayList<>();
+ for (final LALParser.ValueAccessSegmentContext seg : ctx.valueAccessSegment()) {
+ if (seg instanceof LALParser.SegmentFieldContext) {
+ final String name =
+ ((LALParser.SegmentFieldContext) seg).anyIdentifier().getText();
+ segments.add(name);
+ chain.add(new FieldSegment(name, false));
+ } else if (seg instanceof LALParser.SegmentSafeFieldContext) {
+ final String name =
+ ((LALParser.SegmentSafeFieldContext) seg).anyIdentifier().getText();
+ segments.add(name);
+ chain.add(new FieldSegment(name, true));
+ } else if (seg instanceof LALParser.SegmentMethodContext) {
+ final LALParser.FunctionInvocationContext fi =
+ ((LALParser.SegmentMethodContext) seg).functionInvocation();
+ segments.add(fi.IDENTIFIER().getText() + "()");
+ chain.add(new LALScriptModel.MethodSegment(
+ fi.IDENTIFIER().getText(), List.of(), false));
+ } else if (seg instanceof LALParser.SegmentSafeMethodContext) {
+ final LALParser.FunctionInvocationContext fi =
+ ((LALParser.SegmentSafeMethodContext) seg).functionInvocation();
+ segments.add(fi.IDENTIFIER().getText() + "()");
+ chain.add(new LALScriptModel.MethodSegment(
+ fi.IDENTIFIER().getText(), List.of(), true));
+ }
+ }
+
+ return new ValueAccess(segments, parsedRef, logRef, chain);
+ }
+
+ private static String resolveValueAsString(final LALParser.ValueAccessContext ctx) {
+ final LALParser.ValueAccessPrimaryContext primary = ctx.valueAccessPrimary();
+ if (primary instanceof LALParser.ValueStringContext) {
+ return stripQuotes(((LALParser.ValueStringContext) primary).STRING().getText());
+ }
+ return primary.getText();
+ }
+
+ // ==================== Utilities ====================
+
+ private static String extractCastType(final LALParser.TypeCastContext ctx) {
+ if (ctx.STRING_TYPE() != null) {
+ return "String";
+ }
+ if (ctx.LONG_TYPE() != null) {
+ return "Long";
+ }
+ if (ctx.INTEGER_TYPE() != null) {
+ return "Integer";
+ }
+ if (ctx.BOOLEAN_TYPE() != null) {
+ return "Boolean";
+ }
+ return null;
+ }
+
+ static String stripQuotes(final String s) {
+ if (s == null || s.length() < 2) {
+ return s;
+ }
+ final char first = s.charAt(0);
+ if ((first == '\'' || first == '"') && s.charAt(s.length() - 1) == first) {
+ return s.substring(1, s.length() - 1);
+ }
+ // Handle slashy strings: $/ ... /$
+ if (s.startsWith("$/") && s.endsWith("/$")) {
+ return s.substring(2, s.length() - 2);
+ }
+ return s;
+ }
+
+ private static String truncate(final String s, final int maxLen) {
+ if (s.length() <= maxLen) {
+ return s;
+ }
+ return s.substring(0, maxLen) + "...";
+ }
+}
diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/MalExpression.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/rt/BindingAware.java
similarity index 71%
copy from oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/MalExpression.java
copy to oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/rt/BindingAware.java
index cfaf1d5bee..628e7f4874 100644
--- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/MalExpression.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/rt/BindingAware.java
@@ -13,18 +13,19 @@
* 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;
+package org.apache.skywalking.oap.log.analyzer.compiler.rt;
-import java.util.Map;
+import org.apache.skywalking.oap.log.analyzer.dsl.Binding;
/**
- * Pure Java replacement for Groovy-based MAL DelegatingScript.
- * Each transpiled MAL expression implements this interface.
+ * Interface for generated consumer classes that need access to the
+ * current {@link Binding} at runtime.
*/
-@FunctionalInterface
-public interface MalExpression {
- SampleFamily run(Map<String, SampleFamily> samples);
+public interface BindingAware {
+
+ void setBinding(Binding binding);
+
+ Binding getBinding();
}
diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/MalExpression.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/rt/LalExpressionPackageHolder.java
similarity index 72%
copy from oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/MalExpression.java
copy to oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/rt/LalExpressionPackageHolder.java
index cfaf1d5bee..815284cf2b 100644
--- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/MalExpression.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/rt/LalExpressionPackageHolder.java
@@ -13,18 +13,14 @@
* 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.util.Map;
+package org.apache.skywalking.oap.log.analyzer.compiler.rt;
/**
- * Pure Java replacement for Groovy-based MAL DelegatingScript.
- * Each transpiled MAL expression implements this interface.
+ * Empty marker class used as the class loading anchor for Javassist
+ * {@code CtClass.toClass(Class)} on JDK 16+.
+ * Generated LAL expression classes are loaded in this package.
*/
-@FunctionalInterface
-public interface MalExpression {
- SampleFamily run(Map<String, SampleFamily> samples);
+public class LalExpressionPackageHolder {
}
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/Binding.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/Binding.java
index 763dd64d28..ee2beaa474 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/Binding.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/Binding.java
@@ -13,15 +13,13 @@
* 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 com.google.protobuf.Descriptors;
import com.google.protobuf.Message;
-import groovy.lang.Closure;
-import groovy.lang.GroovyObjectSupport;
-import groovy.lang.MissingPropertyException;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -31,35 +29,38 @@ import lombok.Getter;
import org.apache.skywalking.apm.network.logging.v3.LogData;
import org.apache.skywalking.oap.meter.analyzer.dsl.SampleFamily;
import org.apache.skywalking.oap.server.analyzer.provider.trace.parser.listener.DatabaseSlowStatementBuilder;
-
import org.apache.skywalking.oap.server.analyzer.provider.trace.parser.listener.SampledTraceBuilder;
import org.apache.skywalking.oap.server.core.source.Log;
/**
- * The binding bridge between OAP and the DSL, which provides some convenient methods to ease the use of the raw {@link groovy.lang.Binding#setProperty(java.lang.String, java.lang.Object)} and {@link
- * groovy.lang.Binding#getProperty(java.lang.String)}.
+ * Same-FQCN replacement for upstream Binding.
+ * Pure Java implementation that does not extend groovy.lang.Binding.
+ * Uses a simple HashMap for property storage instead of Groovy's binding mechanism.
*/
-public class Binding extends groovy.lang.Binding {
+public class Binding {
public static final String KEY_LOG = "log";
-
public static final String KEY_PARSED = "parsed";
-
public static final String KEY_SAVE = "save";
-
public static final String KEY_ABORT = "abort";
-
public static final String KEY_METRICS_CONTAINER = "metrics_container";
-
public static final String KEY_LOG_CONTAINER = "log_container";
-
public static final String KEY_DATABASE_SLOW_STATEMENT = "database_slow_statement";
-
public static final String KEY_SAMPLED_TRACE = "sampled_trace";
+ private final Map<String, Object> properties = new HashMap<>();
+
public Binding() {
setProperty(KEY_PARSED, new Parsed());
}
+ public void setProperty(final String name, final Object value) {
+ properties.put(name, value);
+ }
+
+ public Object getProperty(final String name) {
+ return properties.get(name);
+ }
+
public Binding log(final LogData.Builder log) {
setProperty(KEY_LOG, log);
setProperty(KEY_SAVE, true);
@@ -105,7 +106,7 @@ public class Binding extends groovy.lang.Binding {
return (DatabaseSlowStatementBuilder) getProperty(KEY_DATABASE_SLOW_STATEMENT);
}
- public Binding databaseSlowStatement(DatabaseSlowStatementBuilder databaseSlowStatementBuilder) {
+ public Binding databaseSlowStatement(final DatabaseSlowStatementBuilder databaseSlowStatementBuilder) {
setProperty(KEY_DATABASE_SLOW_STATEMENT, databaseSlowStatementBuilder);
return this;
}
@@ -114,7 +115,7 @@ public class Binding extends groovy.lang.Binding {
return (SampledTraceBuilder) getProperty(KEY_SAMPLED_TRACE);
}
- public Binding sampledTrace(SampledTraceBuilder sampledTraceBuilder) {
+ public Binding sampledTrace(final SampledTraceBuilder sampledTraceBuilder) {
setProperty(KEY_SAMPLED_TRACE, sampledTraceBuilder);
return this;
}
@@ -142,41 +143,27 @@ public class Binding extends groovy.lang.Binding {
return (boolean) getProperty(KEY_ABORT);
}
- /**
- * Set the metrics container to store all metrics generated from the pipeline,
- * if no container is set, all generated metrics will be sent to MAL engine for further processing,
- * if metrics container is set, all metrics are only stored in the container, and won't be sent to MAL.
- *
- * @param container the metrics container
- */
- public Binding metricsContainer(List<SampleFamily> container) {
+ public Binding metricsContainer(final List<SampleFamily> container) {
setProperty(KEY_METRICS_CONTAINER, container);
return this;
}
+ @SuppressWarnings("unchecked")
public Optional<List<SampleFamily>> metricsContainer() {
- // noinspection unchecked
return Optional.ofNullable((List<SampleFamily>) getProperty(KEY_METRICS_CONTAINER));
}
- /**
- * Set the log container to store the final log if it should be persisted in storage,
- * if no container is set, the final log will be sent to source receiver,
- * if log container is set, the log is only stored in the container, and won't be sent to source receiver.
- *
- * @param container the log container
- */
- public Binding logContainer(AtomicReference<Log> container) {
+ public Binding logContainer(final AtomicReference<Log> container) {
setProperty(KEY_LOG_CONTAINER, container);
return this;
}
+ @SuppressWarnings("unchecked")
public Optional<AtomicReference<Log>> logContainer() {
- // noinspection unchecked
return Optional.ofNullable((AtomicReference<Log>) getProperty(KEY_LOG_CONTAINER));
}
- public static class Parsed extends GroovyObjectSupport {
+ public static class Parsed {
@Getter
private Matcher matcher;
@@ -206,17 +193,20 @@ public class Binding extends groovy.lang.Binding {
return null;
}
- @SuppressWarnings("unused")
- public Object propertyMissing(final String name) {
- return getAt(name);
- }
-
- static Object getField(Object obj, String name) {
- try {
- Closure<?> c = new Closure<Object>(obj, obj) {
- };
- return c.getProperty(name);
- } catch (MissingPropertyException ignored) {
+ static Object getField(final Object obj, final String name) {
+ if (obj instanceof Message) {
+ final Descriptors.FieldDescriptor fd =
+ ((Message) obj).getDescriptorForType().findFieldByName(name);
+ if (fd != null) {
+ return ((Message) obj).getField(fd);
+ }
+ }
+ if (obj instanceof Message.Builder) {
+ final Descriptors.FieldDescriptor fd =
+ ((Message.Builder) obj).getDescriptorForType().findFieldByName(name);
+ if (fd != null) {
+ return ((Message.Builder) obj).getField(fd);
+ }
}
return null;
}
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/DSL.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/DSL.java
index c5666d8061..b3f4e4c977 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/DSL.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/DSL.java
@@ -13,97 +13,130 @@
* 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 com.google.common.collect.ImmutableList;
-
-import groovy.lang.GString;
-import groovy.lang.GroovyShell;
-import groovy.transform.CompileStatic;
-import groovy.util.DelegatingScript;
-import java.lang.reflect.Array;
-import java.util.List;
+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 org.apache.skywalking.oap.log.analyzer.dsl.spec.LALDelegatingScript;
+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.meter.analyzer.dsl.registry.ProcessRegistry;
import org.apache.skywalking.oap.server.library.module.ModuleManager;
import org.apache.skywalking.oap.server.library.module.ModuleStartException;
-import org.codehaus.groovy.ast.stmt.DoWhileStatement;
-import org.codehaus.groovy.ast.stmt.ForStatement;
-import org.codehaus.groovy.ast.stmt.Statement;
-import org.codehaus.groovy.ast.stmt.WhileStatement;
-import org.codehaus.groovy.control.CompilerConfiguration;
-import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer;
-import org.codehaus.groovy.control.customizers.ImportCustomizer;
-import org.codehaus.groovy.control.customizers.SecureASTCustomizer;
-
-import static java.util.Collections.singletonList;
-import static java.util.Collections.singletonMap;
+/**
+ * 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 final DelegatingScript script;
+ 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 CompilerConfiguration cc = new CompilerConfiguration();
- final ASTTransformationCustomizer customizer =
- new ASTTransformationCustomizer(
- singletonMap(
- "extensions",
- singletonList(LALPrecompiledExtension.class.getName())
- ),
- CompileStatic.class
- );
- cc.addCompilationCustomizers(customizer);
- final SecureASTCustomizer secureASTCustomizer = new SecureASTCustomizer();
- secureASTCustomizer.setDisallowedStatements(
- ImmutableList.<Class<? extends Statement>>builder()
- .add(WhileStatement.class)
- .add(DoWhileStatement.class)
- .add(ForStatement.class)
- .build());
- // noinspection rawtypes
- secureASTCustomizer.setAllowedReceiversClasses(
- ImmutableList.<Class>builder()
- .add(Object.class)
- .add(Map.class)
- .add(List.class)
- .add(Array.class)
- .add(GString.class)
- .add(String.class)
- .add(ProcessRegistry.class)
- .build());
- cc.addCompilationCustomizers(secureASTCustomizer);
- cc.setScriptBaseClass(LALDelegatingScript.class.getName());
-
- ImportCustomizer icz = new ImportCustomizer();
- icz.addImport("ProcessRegistry", ProcessRegistry.class.getName());
- cc.addCompilationCustomizers(icz);
+ 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.");
+ }
- final GroovyShell sh = new GroovyShell(cc);
- final DelegatingScript script = (DelegatingScript) sh.parse(dsl);
- final FilterSpec filterSpec = new FilterSpec(moduleManager, config);
- script.setDelegate(filterSpec);
-
- return new DSL(script, filterSpec);
+ 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() {
- script.run();
+ 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/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/AbstractSpec.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/AbstractSpec.java
index d815095776..6b8e947cb8 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/AbstractSpec.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/AbstractSpec.java
@@ -18,7 +18,7 @@
package org.apache.skywalking.oap.log.analyzer.dsl.spec;
-import groovy.lang.Closure;
+import java.util.function.Consumer;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
@@ -42,15 +42,10 @@ public abstract class AbstractSpec {
}
@SuppressWarnings("unused")
- public void abort(final Closure<Void> cl) {
+ public void abort(final Consumer<Void> cl) {
BINDING.get().abort();
}
- @SuppressWarnings("unused")
- public Object propertyMissing(final String name) {
- return BINDING.get().getVariable(name);
- }
-
@SuppressWarnings("unused")
public String tag(String key) {
return BINDING.get().log().getTags().getDataList()
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/extractor/ExtractorSpec.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/extractor/ExtractorSpec.java
index 2a51d10d4f..02c0186701 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/extractor/ExtractorSpec.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/extractor/ExtractorSpec.java
@@ -20,8 +20,6 @@ package org.apache.skywalking.oap.log.analyzer.dsl.spec.extractor;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
-import groovy.lang.Closure;
-import groovy.lang.DelegatesTo;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collection;
@@ -241,99 +239,6 @@ public class ExtractorSpec extends AbstractSpec {
}
}
- @SuppressWarnings("unused")
- public void metrics(@DelegatesTo(SampleBuilder.class) final Closure<?> cl) {
- if (BINDING.get().shouldAbort()) {
- return;
- }
- final SampleBuilder builder = new SampleBuilder();
- cl.setDelegate(builder);
- cl.call();
-
- final Sample sample = builder.build();
- final SampleFamily sampleFamily = SampleFamilyBuilder.newBuilder(sample).build();
-
- final Optional<List<SampleFamily>> possibleMetricsContainer = BINDING.get().metricsContainer();
-
- if (possibleMetricsContainer.isPresent()) {
- possibleMetricsContainer.get().add(sampleFamily);
- } else {
- metricConverts.forEach(it -> it.toMeter(
- ImmutableMap.<String, SampleFamily>builder()
- .put(sample.getName(), sampleFamily)
- .build()
- ));
- }
- }
-
- @SuppressWarnings("unused")
- public void slowSql(@DelegatesTo(SlowSqlSpec.class) final Closure<?> cl) {
- if (BINDING.get().shouldAbort()) {
- return;
- }
- LogData.Builder log = BINDING.get().log();
- if (log.getLayer() == null
- || log.getService() == null
- || log.getTimestamp() < 1) {
- LOGGER.warn("SlowSql extracts failed, maybe something is not configured.");
- return;
- }
- DatabaseSlowStatementBuilder builder = new DatabaseSlowStatementBuilder(namingControl);
- builder.setLayer(Layer.nameOf(log.getLayer()));
-
- builder.setServiceName(log.getService());
-
- BINDING.get().databaseSlowStatement(builder);
-
- cl.setDelegate(slowSql);
- cl.call();
-
- if (builder.getId() == null
- || builder.getLatency() < 1
- || builder.getStatement() == null) {
- LOGGER.warn("SlowSql extracts failed, maybe something is not configured.");
- return;
- }
-
- long timeBucketForDB = TimeBucket.getTimeBucket(log.getTimestamp(), DownSampling.Second);
- builder.setTimeBucket(timeBucketForDB);
- builder.setTimestamp(log.getTimestamp());
-
- builder.prepare();
- sourceReceiver.receive(builder.toDatabaseSlowStatement());
-
- ServiceMeta serviceMeta = new ServiceMeta();
- serviceMeta.setName(builder.getServiceName());
- serviceMeta.setLayer(builder.getLayer());
- long timeBucket = TimeBucket.getTimeBucket(log.getTimestamp(), DownSampling.Minute);
- serviceMeta.setTimeBucket(timeBucket);
- sourceReceiver.receive(serviceMeta);
- }
-
- @SuppressWarnings("unused")
- public void sampledTrace(@DelegatesTo(SampledTraceSpec.class) final Closure<?> cl) {
- if (BINDING.get().shouldAbort()) {
- return;
- }
- LogData.Builder log = BINDING.get().log();
- SampledTraceBuilder builder = new SampledTraceBuilder(namingControl);
- builder.setLayer(log.getLayer());
- builder.setTimestamp(log.getTimestamp());
- builder.setServiceName(log.getService());
- builder.setServiceInstanceName(log.getServiceInstance());
- builder.setTraceId(log.getTraceContext().getTraceId());
- BINDING.get().sampledTrace(builder);
-
- cl.setDelegate(sampledTrace);
- cl.call();
-
- builder.validate();
- final Record record = builder.toRecord();
- final ISource entity = builder.toEntity();
- RecordStreamProcessor.getInstance().in(record);
- sourceReceiver.receive(entity);
- }
-
public void metrics(final Consumer<SampleBuilder> consumer) {
if (BINDING.get().shouldAbort()) {
return;
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/filter/FilterSpec.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/filter/FilterSpec.java
index b83e71b8b8..c9e3338df6 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/filter/FilterSpec.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/filter/FilterSpec.java
@@ -21,8 +21,6 @@ package org.apache.skywalking.oap.log.analyzer.dsl.spec.filter;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.protobuf.Message;
import com.google.protobuf.TextFormat;
-import groovy.lang.Closure;
-import groovy.lang.DelegatesTo;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@@ -87,110 +85,6 @@ public class FilterSpec extends AbstractSpec {
sink = new SinkSpec(moduleManager(), moduleConfig());
}
- @SuppressWarnings("unused")
- public void text(@DelegatesTo(TextParserSpec.class) final Closure<?> cl) {
- if (BINDING.get().shouldAbort()) {
- return;
- }
- cl.setDelegate(textParser);
- cl.call();
- }
-
- @SuppressWarnings("unused")
- public void json(@DelegatesTo(JsonParserSpec.class) final Closure<?> cl) {
- if (BINDING.get().shouldAbort()) {
- return;
- }
- cl.setDelegate(jsonParser);
- cl.call();
-
- final LogData.Builder logData = BINDING.get().log();
- try {
-
- final Map<String, Object> parsed = jsonParser.create().readValue(
- logData.getBody().getJson().getJson(), parsedType
- );
-
- BINDING.get().parsed(parsed);
- } catch (final Exception e) {
- if (jsonParser.abortOnFailure()) {
- BINDING.get().abort();
- }
- }
- }
-
- @SuppressWarnings({"unused"})
- public void yaml(@DelegatesTo(YamlParserSpec.class) final Closure<?> cl) {
- if (BINDING.get().shouldAbort()) {
- return;
- }
- cl.setDelegate(yamlParser);
- cl.call();
-
- final LogData.Builder logData = BINDING.get().log();
- try {
- final Map<String, Object> parsed = yamlParser.create().load(
- logData.getBody().getYaml().getYaml()
- );
-
- BINDING.get().parsed(parsed);
- } catch (final Exception e) {
- if (yamlParser.abortOnFailure()) {
- BINDING.get().abort();
- }
- }
- }
-
- @SuppressWarnings("unused")
- public void extractor(@DelegatesTo(ExtractorSpec.class) final Closure<?> cl) {
- if (BINDING.get().shouldAbort()) {
- return;
- }
- cl.setDelegate(extractor);
- cl.call();
- }
-
- @SuppressWarnings("unused")
- public void sink(@DelegatesTo(SinkSpec.class) final Closure<?> cl) {
- if (BINDING.get().shouldAbort()) {
- return;
- }
- cl.setDelegate(sink);
- cl.call();
-
- final Binding b = BINDING.get();
- final LogData.Builder logData = b.log();
- final Message extraLog = b.extraLog();
-
- if (!b.shouldSave()) {
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("Log is dropped: {}", TextFormat.shortDebugString(logData));
- }
- return;
- }
-
- final Optional<AtomicReference<Log>> container = BINDING.get().logContainer();
- if (container.isPresent()) {
- sinkListenerFactories.stream()
- .map(LogSinkListenerFactory::create)
- .filter(it -> it instanceof RecordSinkListener)
- .map(it -> it.parse(logData, extraLog))
- .map(it -> (RecordSinkListener) it)
- .map(RecordSinkListener::getLog)
- .findFirst()
- .ifPresent(log -> container.get().set(log));
- } else {
- sinkListenerFactories.stream()
- .map(LogSinkListenerFactory::create)
- .forEach(it -> it.parse(logData, extraLog).build());
- }
- }
-
- @SuppressWarnings("unused")
- public void filter(final Closure<?> cl) {
- cl.call();
- }
-
public void text(final Consumer<TextParserSpec> consumer) {
if (BINDING.get().shouldAbort()) {
return;
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 f10d471ee1..7e1ff921ad 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
@@ -18,21 +18,16 @@
package org.apache.skywalking.oap.log.analyzer.dsl.spec.sink;
-import groovy.lang.Closure;
-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;
import org.apache.skywalking.oap.log.analyzer.dsl.spec.sink.sampler.Sampler;
import org.apache.skywalking.oap.log.analyzer.provider.LogAnalyzerModuleConfig;
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;
@@ -41,26 +36,11 @@ public class SamplerSpec extends AbstractSpec {
final LogAnalyzerModuleConfig moduleConfig) {
super(moduleManager, moduleConfig);
- rateLimitSamplers = new ConcurrentHashMap<>();
rateLimitSamplersByString = new ConcurrentHashMap<>();
possibilitySamplers = new ConcurrentHashMap<>();
rlsResetHandler = new RateLimitingSampler.ResetHandler();
}
- @SuppressWarnings("unused")
- public void rateLimit(final GString id, @DelegatesTo(RateLimitingSampler.class) final Closure<?> cl) {
- if (BINDING.get().shouldAbort()) {
- return;
- }
-
- final Sampler sampler = rateLimitSamplers.computeIfAbsent(id, $ -> new RateLimitingSampler(rlsResetHandler).start());
-
- cl.setDelegate(sampler);
- cl.call();
-
- sampleWith(sampler);
- }
-
@SuppressWarnings("unused")
public void rateLimit(final String id, final Consumer<RateLimitingSampler> consumer) {
if (BINDING.get().shouldAbort()) {
@@ -75,20 +55,6 @@ public class SamplerSpec extends AbstractSpec {
sampleWith(sampler);
}
- @SuppressWarnings("unused")
- public void possibility(final int percentage, @DelegatesTo(PossibilitySampler.class) final Closure<?> cl) {
- if (BINDING.get().shouldAbort()) {
- return;
- }
-
- final Sampler sampler = possibilitySamplers.computeIfAbsent(percentage, $ -> new PossibilitySampler(percentage).start());
-
- cl.setDelegate(sampler);
- cl.call();
-
- sampleWith(sampler);
- }
-
private void sampleWith(final Sampler sampler) {
if (BINDING.get().shouldAbort()) {
return;
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/sink/SinkSpec.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/sink/SinkSpec.java
index f2ae371a21..631cccfca9 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/sink/SinkSpec.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/spec/sink/SinkSpec.java
@@ -18,8 +18,6 @@
package org.apache.skywalking.oap.log.analyzer.dsl.spec.sink;
-import groovy.lang.Closure;
-import groovy.lang.DelegatesTo;
import java.util.function.Consumer;
import org.apache.skywalking.oap.log.analyzer.dsl.spec.AbstractSpec;
import org.apache.skywalking.oap.log.analyzer.provider.LogAnalyzerModuleConfig;
@@ -36,15 +34,6 @@ public class SinkSpec extends AbstractSpec {
sampler = new SamplerSpec(moduleManager(), moduleConfig());
}
- @SuppressWarnings("unused")
- public void sampler(@DelegatesTo(SamplerSpec.class) final Closure<?> cl) {
- if (BINDING.get().shouldAbort()) {
- return;
- }
- cl.setDelegate(sampler);
- cl.call();
- }
-
public void sampler(final Consumer<SamplerSpec> consumer) {
if (BINDING.get().shouldAbort()) {
return;
@@ -52,14 +41,6 @@ public class SinkSpec extends AbstractSpec {
consumer.accept(sampler);
}
- @SuppressWarnings("unused")
- public void enforcer(final Closure<?> cl) {
- if (BINDING.get().shouldAbort()) {
- return;
- }
- BINDING.get().save();
- }
-
public void enforcer() {
if (BINDING.get().shouldAbort()) {
return;
@@ -67,14 +48,6 @@ public class SinkSpec extends AbstractSpec {
BINDING.get().save();
}
- @SuppressWarnings("unused")
- public void dropper(final Closure<?> cl) {
- if (BINDING.get().shouldAbort()) {
- return;
- }
- BINDING.get().drop();
- }
-
public void dropper() {
if (BINDING.get().shouldAbort()) {
return;
diff --git a/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/compiler/LALClassGeneratorTest.java b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/compiler/LALClassGeneratorTest.java
new file mode 100644
index 0000000000..7bde461a4f
--- /dev/null
+++ b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/compiler/LALClassGeneratorTest.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.log.analyzer.compiler;
+
+import javassist.ClassPool;
+import org.apache.skywalking.oap.log.analyzer.dsl.LalExpression;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class LALClassGeneratorTest {
+
+ private LALClassGenerator generator;
+
+ @BeforeEach
+ void setUp() {
+ generator = new LALClassGenerator(new ClassPool(true));
+ }
+
+ @Test
+ void compileMinimalFilter() throws Exception {
+ final LalExpression expr = generator.compile(
+ "filter { sink {} }");
+ assertNotNull(expr);
+ }
+
+ @Test
+ void compileJsonParserFilter() throws Exception {
+ final LalExpression expr = generator.compile(
+ "filter { json {} sink {} }");
+ assertNotNull(expr);
+ }
+
+ @Test
+ void compileJsonWithExtractor() throws Exception {
+ final LalExpression expr = generator.compile(
+ "filter {\n"
+ + " json {}\n"
+ + " extractor {\n"
+ + " service parsed.service as String\n"
+ + " }\n"
+ + " sink {}\n"
+ + "}");
+ assertNotNull(expr);
+ }
+
+ @Test
+ void compileTextWithRegexp() throws Exception {
+ final LalExpression expr = generator.compile(
+ "filter {\n"
+ + " text {\n"
+ + " regexp '(?<timestamp>\\\\d+) (?<level>\\\\w+) (?<msg>.*)'\n"
+ + " }\n"
+ + " sink {}\n"
+ + "}");
+ assertNotNull(expr);
+ }
+
+ @Test
+ void compileSinkWithEnforcer() throws Exception {
+ final LalExpression expr = generator.compile(
+ "filter {\n"
+ + " json {}\n"
+ + " sink {\n"
+ + " enforcer {}\n"
+ + " }\n"
+ + "}");
+ assertNotNull(expr);
+ }
+
+ @Test
+ void generateSourceReturnsJavaCode() {
+ final String source = generator.generateSource(
+ "filter { json {} sink {} }");
+ assertNotNull(source);
+ org.junit.jupiter.api.Assertions.assertTrue(
+ source.contains("filterSpec.json()"));
+ org.junit.jupiter.api.Assertions.assertTrue(
+ source.contains("filterSpec.sink()"));
+ }
+}
diff --git a/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptParserTest.java b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptParserTest.java
new file mode 100644
index 0000000000..5c29397f8c
--- /dev/null
+++ b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptParserTest.java
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.log.analyzer.compiler;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+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 LALScriptParserTest {
+
+ @Test
+ void parseMinimalFilter() {
+ final LALScriptModel model = LALScriptParser.parse("filter { sink {} }");
+ assertNotNull(model);
+ assertEquals(1, model.getStatements().size());
+ assertInstanceOf(LALScriptModel.SinkBlock.class, model.getStatements().get(0));
+ }
+
+ @Test
+ void parseJsonParserWithExtractorAndSink() {
+ final LALScriptModel model = LALScriptParser.parse(
+ "filter {\n"
+ + " json {}\n"
+ + " extractor {\n"
+ + " service parsed.service as String\n"
+ + " layer parsed.layer as String\n"
+ + " }\n"
+ + " sink {}\n"
+ + "}");
+
+ assertEquals(3, model.getStatements().size());
+ assertInstanceOf(LALScriptModel.JsonParser.class, model.getStatements().get(0));
+
+ final LALScriptModel.ExtractorBlock extractor =
+ (LALScriptModel.ExtractorBlock) model.getStatements().get(1);
+ assertEquals(2, extractor.getStatements().size());
+
+ final LALScriptModel.FieldAssignment serviceField =
+ (LALScriptModel.FieldAssignment) extractor.getStatements().get(0);
+ assertEquals(LALScriptModel.FieldType.SERVICE, serviceField.getFieldType());
+ assertTrue(serviceField.getValue().isParsedRef());
+ assertEquals("String", serviceField.getCastType());
+ }
+
+ @Test
+ void parseTextParserWithRegexp() {
+ final LALScriptModel model = LALScriptParser.parse(
+ "filter {\n"
+ + " text {\n"
+ + " regexp '.+\"(?<request>.+)\"(?<status>\\d{3}).+'\n"
+ + " }\n"
+ + " sink {}\n"
+ + "}");
+
+ assertEquals(2, model.getStatements().size());
+ final LALScriptModel.TextParser textParser =
+ (LALScriptModel.TextParser) model.getStatements().get(0);
+ assertNotNull(textParser.getRegexpPattern());
+ }
+
+ @Test
+ void parseSlowSql() {
+ final LALScriptModel model = LALScriptParser.parse(
+ "filter {\n"
+ + " json {}\n"
+ + " extractor {\n"
+ + " layer parsed.layer as String\n"
+ + " service parsed.service as String\n"
+ + " timestamp parsed.time as String\n"
+ + " slowSql {\n"
+ + " id parsed.id as String\n"
+ + " statement parsed.statement as String\n"
+ + " latency parsed.query_time as Long\n"
+ + " }\n"
+ + " }\n"
+ + "}");
+
+ final LALScriptModel.ExtractorBlock extractor =
+ (LALScriptModel.ExtractorBlock) model.getStatements().get(1);
+
+ // Find the slowSql block
+ LALScriptModel.SlowSqlBlock slowSql = null;
+ for (final LALScriptModel.ExtractorStatement stmt : extractor.getStatements()) {
+ if (stmt instanceof LALScriptModel.SlowSqlBlock) {
+ slowSql = (LALScriptModel.SlowSqlBlock) stmt;
+ }
+ }
+ assertNotNull(slowSql);
+ assertNotNull(slowSql.getId());
+ assertEquals("String", slowSql.getIdCast());
+ assertNotNull(slowSql.getStatement());
+ assertEquals("String", slowSql.getStatementCast());
+ assertNotNull(slowSql.getLatency());
+ assertEquals("Long", slowSql.getLatencyCast());
+ }
+
+ @Test
+ void parseMetricsBlock() {
+ final LALScriptModel model = LALScriptParser.parse(
+ "filter {\n"
+ + " extractor {\n"
+ + " metrics {\n"
+ + " timestamp log.timestamp as Long\n"
+ + " labels level: parsed.level, service: log.service\n"
+ + " name \"nginx_error_log_count\"\n"
+ + " value 1\n"
+ + " }\n"
+ + " }\n"
+ + " sink {}\n"
+ + "}");
+
+ final LALScriptModel.ExtractorBlock extractor =
+ (LALScriptModel.ExtractorBlock) model.getStatements().get(0);
+ final LALScriptModel.MetricsBlock metrics =
+ (LALScriptModel.MetricsBlock) extractor.getStatements().get(0);
+
+ assertEquals("nginx_error_log_count", metrics.getName());
+ assertEquals(2, metrics.getLabels().size());
+ assertTrue(metrics.getLabels().containsKey("level"));
+ assertTrue(metrics.getLabels().containsKey("service"));
+ assertNotNull(metrics.getValue());
+ }
+
+ @Test
+ void parseSinkWithSampler() {
+ final LALScriptModel model = LALScriptParser.parse(
+ "filter {\n"
+ + " sink {\n"
+ + " sampler {\n"
+ + " rateLimit('service:error') {\n"
+ + " rpm 6000\n"
+ + " }\n"
+ + " }\n"
+ + " }\n"
+ + "}");
+
+ final LALScriptModel.SinkBlock sink =
+ (LALScriptModel.SinkBlock) model.getStatements().get(0);
+ assertEquals(1, sink.getStatements().size());
+ final LALScriptModel.SamplerBlock sampler =
+ (LALScriptModel.SamplerBlock) sink.getStatements().get(0);
+ assertEquals(1, sampler.getContents().size());
+ final LALScriptModel.RateLimitBlock rateLimit =
+ (LALScriptModel.RateLimitBlock) sampler.getContents().get(0);
+ assertEquals("service:error", rateLimit.getId());
+ assertEquals(6000, rateLimit.getRpm());
+ }
+
+ @Test
+ void parseIfCondition() {
+ final LALScriptModel model = LALScriptParser.parse(
+ "filter {\n"
+ + " if (parsed.status) {\n"
+ + " extractor {\n"
+ + " layer parsed.layer as String\n"
+ + " }\n"
+ + " sink {}\n"
+ + " }\n"
+ + "}");
+
+ assertEquals(1, model.getStatements().size());
+ final LALScriptModel.IfBlock ifBlock =
+ (LALScriptModel.IfBlock) model.getStatements().get(0);
+ assertNotNull(ifBlock.getCondition());
+ assertEquals(2, ifBlock.getThenBranch().size());
+ }
+
+ @Test
+ void parseSyntaxErrorThrows() {
+ assertThrows(IllegalArgumentException.class,
+ () -> LALScriptParser.parse("filter {"));
+ }
+}
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/src/test/java/org/apache/skywalking/oap/log/analyzer/dsl/DSLV2Test.java
similarity index 100%
rename from oap-server/analyzer/log-analyzer-v2/src/test/java/org/apache/skywalking/oap/log/analyzer/dsl/DSLV2Test.java
rename to oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/dsl/DSLV2Test.java
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
deleted file mode 100644
index c1deeb2a04..0000000000
--- a/oap-server/analyzer/mal-transpiler/src/main/java/org/apache/skywalking/oap/server/transpiler/mal/MalToJavaTranspiler.java
+++ /dev/null
@@ -1,1099 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.skywalking.oap.server.transpiler.mal;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.StringWriter;
-import java.nio.file.Files;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-import javax.tools.JavaCompiler;
-import javax.tools.JavaFileObject;
-import javax.tools.StandardJavaFileManager;
-import javax.tools.ToolProvider;
-import lombok.extern.slf4j.Slf4j;
-import org.codehaus.groovy.ast.ClassNode;
-import org.codehaus.groovy.ast.ModuleNode;
-import org.codehaus.groovy.ast.Parameter;
-import org.codehaus.groovy.ast.expr.ArgumentListExpression;
-import org.codehaus.groovy.ast.expr.BinaryExpression;
-import org.codehaus.groovy.ast.expr.ClosureExpression;
-import org.codehaus.groovy.ast.expr.ClassExpression;
-import org.codehaus.groovy.ast.expr.ConstantExpression;
-import org.codehaus.groovy.ast.expr.DeclarationExpression;
-import org.codehaus.groovy.ast.expr.ElvisOperatorExpression;
-import org.codehaus.groovy.ast.expr.Expression;
-import org.codehaus.groovy.ast.expr.ListExpression;
-import org.codehaus.groovy.ast.expr.MapEntryExpression;
-import org.codehaus.groovy.ast.expr.MapExpression;
-import org.codehaus.groovy.ast.expr.MethodCallExpression;
-import org.codehaus.groovy.ast.expr.PropertyExpression;
-import org.codehaus.groovy.ast.expr.TernaryExpression;
-import org.codehaus.groovy.ast.expr.TupleExpression;
-import org.codehaus.groovy.ast.expr.VariableExpression;
-import org.codehaus.groovy.syntax.Types;
-import org.codehaus.groovy.ast.expr.BooleanExpression;
-import org.codehaus.groovy.ast.expr.NotExpression;
-import org.codehaus.groovy.ast.stmt.BlockStatement;
-import org.codehaus.groovy.ast.stmt.EmptyStatement;
-import org.codehaus.groovy.ast.stmt.ExpressionStatement;
-import org.codehaus.groovy.ast.stmt.IfStatement;
-import org.codehaus.groovy.ast.stmt.ReturnStatement;
-import org.codehaus.groovy.ast.stmt.Statement;
-import org.codehaus.groovy.control.CompilationUnit;
-import org.codehaus.groovy.control.CompilerConfiguration;
-import org.codehaus.groovy.control.Phases;
-
-/**
- * Transpiles Groovy MAL expressions to Java source code at build time.
- * Parses expression strings into Groovy AST (via CompilationUnit at CONVERSION phase),
- * walks AST nodes, and produces equivalent Java classes implementing MalExpression or MalFilter.
- *
- * <p>Supported AST patterns:
- * <ul>
- * <li>Variable references: sample family lookups, DownsamplingType constants, KNOWN_TYPES</li>
- * <li>Method chains: .sum(), .service(), .tagEqual(), .rate(), .histogram(), etc.</li>
- * <li>Binary arithmetic with operand-swap logic per upstream ExpandoMetaClass
- * (N-SF: sf.minus(N).negative(), N/SF: sf.newValue(v->N/v))</li>
- * <li>tag() closures: TagFunction lambda (assignment, remove, string concat, if/else)</li>
- * <li>Filter closures: MalFilter class (==, !=, in, truthiness, negation, &&, ||)</li>
- * <li>forEach() closures: ForEachFunction lambda (var decls, if/else-if, early return)</li>
- * <li>instance() with PropertiesExtractor closure: Map.of() from map literals</li>
- * <li>Elvis (?:), safe navigation (?.), ternary (? :)</li>
- * <li>Batch compilation via javax.tools.JavaCompiler + manifest writing</li>
- * </ul>
- */
-@Slf4j
-public class MalToJavaTranspiler {
-
- 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(
- "AVG", "SUM", "LATEST", "SUM_PER_MIN", "MAX", "MIN"
- );
-
- private static final Set<String> KNOWN_TYPES = Set.of(
- "Layer", "DetectPoint", "K8sRetagType", "ProcessRegistry", "TimeUnit"
- );
-
- // ---- Batch state tracking ----
-
- private final Map<String, String> expressionSources = new LinkedHashMap<>();
-
- private final Map<String, String> filterSources = new LinkedHashMap<>();
-
- private final Map<String, String> filterLiteralToClass = new LinkedHashMap<>();
-
- /**
- * Transpile a MAL expression to a Java class source implementing MalExpression.
- *
- * @param className simple class name (e.g. "MalExpr_meter_jvm_heap")
- * @param expression the Groovy expression string
- * @return generated Java source code
- */
- public String transpileExpression(final String className, final String expression) {
- final ModuleNode ast = parseToAST(expression);
- final Statement body = extractBody(ast);
-
- final Set<String> sampleNames = new LinkedHashSet<>();
- collectSampleNames(body, sampleNames);
-
- final String javaBody = visitStatement(body);
-
- final StringBuilder sb = new StringBuilder();
- sb.append("package ").append(GENERATED_PACKAGE).append(";\n\n");
- sb.append("import java.util.*;\n");
- sb.append("import org.apache.skywalking.oap.meter.analyzer.dsl.*;\n");
- sb.append("import org.apache.skywalking.oap.meter.analyzer.dsl.SampleFamilyFunctions.*;\n");
- sb.append("import org.apache.skywalking.oap.server.core.analysis.Layer;\n");
- sb.append("import org.apache.skywalking.oap.server.core.source.DetectPoint;\n");
- sb.append("import org.apache.skywalking.oap.meter.analyzer.dsl.tagOpt.K8sRetagType;\n");
- sb.append("import org.apache.skywalking.oap.meter.analyzer.dsl.registry.ProcessRegistry;\n\n");
-
- sb.append("public class ").append(className).append(" implements MalExpression {\n");
- sb.append(" @Override\n");
- sb.append(" public SampleFamily run(Map<String, SampleFamily> samples) {\n");
-
- if (!sampleNames.isEmpty()) {
- sb.append(" ExpressionParsingContext.get().ifPresent(ctx -> {\n");
- for (String name : sampleNames) {
- sb.append(" ctx.getSamples().add(\"").append(escapeJava(name)).append("\");\n");
- }
- sb.append(" });\n");
- }
-
- sb.append(" return ").append(javaBody).append(";\n");
- sb.append(" }\n");
- sb.append("}\n");
-
- return sb.toString();
- }
-
- /**
- * Transpile a MAL filter literal to a Java class source implementing MalFilter.
- * Filter literals are closures like: { tags -> tags.job_name == 'vm-monitoring' }
- *
- * @param className simple class name (e.g. "MalFilter_0")
- * @param filterLiteral the Groovy closure literal string
- * @return generated Java source code
- */
- public String transpileFilter(final String className, final String filterLiteral) {
- final ModuleNode ast = parseToAST(filterLiteral);
- final Statement body = extractBody(ast);
-
- // The filter literal is a closure expression at the top level
- final ClosureExpression closure = extractClosure(body);
- final Parameter[] params = closure.getParameters();
- final String tagsVar = (params != null && params.length > 0) ? params[0].getName() : "tags";
-
- // Get the body expression — may need to unwrap inner block/closure
- final Expression bodyExpr = extractFilterBodyExpr(closure.getCode(), tagsVar);
-
- // Generate the boolean condition
- final String condition = visitFilterCondition(bodyExpr, tagsVar);
-
- final StringBuilder sb = new StringBuilder();
- sb.append("package ").append(GENERATED_PACKAGE).append(";\n\n");
- sb.append("import java.util.*;\n");
- sb.append("import org.apache.skywalking.oap.meter.analyzer.dsl.*;\n\n");
-
- sb.append("public class ").append(className).append(" implements MalFilter {\n");
- sb.append(" @Override\n");
- sb.append(" public boolean test(Map<String, String> tags) {\n");
- sb.append(" return ").append(condition).append(";\n");
- sb.append(" }\n");
- sb.append("}\n");
-
- return sb.toString();
- }
-
- private ClosureExpression extractClosure(final Statement body) {
- final List<Statement> stmts = getStatements(body);
- if (stmts.size() == 1 && stmts.get(0) instanceof ExpressionStatement) {
- final Expression expr = ((ExpressionStatement) stmts.get(0)).getExpression();
- if (expr instanceof ClosureExpression) {
- return (ClosureExpression) expr;
- }
- }
- throw new IllegalStateException(
- "Filter literal must be a single closure expression, got: "
- + (stmts.isEmpty() ? "empty" : stmts.get(0).getClass().getSimpleName()));
- }
-
- private Expression extractFilterBodyExpr(final Statement code, final String tagsVar) {
- final List<Statement> stmts = getStatements(code);
- if (stmts.isEmpty()) {
- throw new IllegalStateException("Empty filter closure body");
- }
-
- final Statement last = stmts.get(stmts.size() - 1);
- Expression expr;
- if (last instanceof ExpressionStatement) {
- expr = ((ExpressionStatement) last).getExpression();
- } else if (last instanceof ReturnStatement) {
- expr = ((ReturnStatement) last).getExpression();
- } else if (last instanceof BlockStatement) {
- return extractFilterBodyExpr(last, tagsVar);
- } else {
- throw new UnsupportedOperationException(
- "Unsupported filter body statement: " + last.getClass().getSimpleName());
- }
-
- if (expr instanceof ClosureExpression) {
- final ClosureExpression inner = (ClosureExpression) expr;
- return extractFilterBodyExpr(inner.getCode(), tagsVar);
- }
-
- return expr;
- }
-
- // ---- AST Parsing ----
-
- ModuleNode parseToAST(final String expression) {
- final CompilerConfiguration cc = new CompilerConfiguration();
- final CompilationUnit cu = new CompilationUnit(cc);
- cu.addSource("Script", expression);
- cu.compile(Phases.CONVERSION);
- final List<ModuleNode> modules = cu.getAST().getModules();
- if (modules.isEmpty()) {
- throw new IllegalStateException("No AST modules produced for: " + expression);
- }
- return modules.get(0);
- }
-
- Statement extractBody(final ModuleNode module) {
- final BlockStatement block = module.getStatementBlock();
- if (block != null && !block.getStatements().isEmpty()) {
- return block;
- }
- final List<ClassNode> classes = module.getClasses();
- if (!classes.isEmpty()) {
- return module.getStatementBlock();
- }
- throw new IllegalStateException("Empty AST body");
- }
-
- // ---- Sample Name Collection ----
-
- private void collectSampleNames(final Statement stmt, final Set<String> names) {
- if (stmt instanceof BlockStatement) {
- for (Statement s : ((BlockStatement) stmt).getStatements()) {
- collectSampleNames(s, names);
- }
- } else if (stmt instanceof ExpressionStatement) {
- collectSampleNamesFromExpr(((ExpressionStatement) stmt).getExpression(), names);
- } else if (stmt instanceof ReturnStatement) {
- collectSampleNamesFromExpr(((ReturnStatement) stmt).getExpression(), names);
- }
- }
-
- void collectSampleNamesFromExpr(final Expression expr, final Set<String> names) {
- if (expr instanceof VariableExpression) {
- final String name = ((VariableExpression) expr).getName();
- if (!DOWNSAMPLING_CONSTANTS.contains(name)
- && !KNOWN_TYPES.contains(name)
- && !name.equals("this") && !name.equals("time")) {
- names.add(name);
- }
- } else if (expr instanceof BinaryExpression) {
- final BinaryExpression bin = (BinaryExpression) expr;
- collectSampleNamesFromExpr(bin.getLeftExpression(), names);
- collectSampleNamesFromExpr(bin.getRightExpression(), names);
- } else if (expr instanceof PropertyExpression) {
- final PropertyExpression pe = (PropertyExpression) expr;
- collectSampleNamesFromExpr(pe.getObjectExpression(), names);
- } else if (expr instanceof MethodCallExpression) {
- final MethodCallExpression mce = (MethodCallExpression) expr;
- collectSampleNamesFromExpr(mce.getObjectExpression(), names);
- collectSampleNamesFromExpr(mce.getArguments(), names);
- } else if (expr instanceof ArgumentListExpression) {
- for (Expression e : ((ArgumentListExpression) expr).getExpressions()) {
- collectSampleNamesFromExpr(e, names);
- }
- } else if (expr instanceof TupleExpression) {
- for (Expression e : ((TupleExpression) expr).getExpressions()) {
- collectSampleNamesFromExpr(e, names);
- }
- }
- }
-
- // ---- Statement Visiting ----
-
- String visitStatement(final Statement stmt) {
- if (stmt instanceof BlockStatement) {
- final List<Statement> stmts = ((BlockStatement) stmt).getStatements();
- if (stmts.size() == 1) {
- return visitStatement(stmts.get(0));
- }
- // Multi-statement: last one is the return value
- return visitStatement(stmts.get(stmts.size() - 1));
- } else if (stmt instanceof ExpressionStatement) {
- return visitExpression(((ExpressionStatement) stmt).getExpression());
- } else if (stmt instanceof ReturnStatement) {
- return visitExpression(((ReturnStatement) stmt).getExpression());
- }
- throw new UnsupportedOperationException(
- "Unsupported statement: " + stmt.getClass().getSimpleName());
- }
-
- // ---- Expression Visiting ----
-
- String visitExpression(final Expression expr) {
- if (expr instanceof VariableExpression) {
- return visitVariable((VariableExpression) expr);
- } else if (expr instanceof ConstantExpression) {
- return visitConstant((ConstantExpression) expr);
- } else if (expr instanceof MethodCallExpression) {
- return visitMethodCall((MethodCallExpression) expr);
- } else if (expr instanceof PropertyExpression) {
- return visitProperty((PropertyExpression) expr);
- } else if (expr instanceof ListExpression) {
- return visitList((ListExpression) expr);
- } else if (expr instanceof BinaryExpression) {
- return visitBinary((BinaryExpression) expr);
- } else if (expr instanceof ClosureExpression) {
- throw new UnsupportedOperationException(
- "Bare ClosureExpression outside method call context: " + expr.getText());
- }
- throw new UnsupportedOperationException(
- "Unsupported expression (not yet implemented): "
- + expr.getClass().getSimpleName() + " = " + expr.getText());
- }
-
- private String visitVariable(final VariableExpression expr) {
- final String name = expr.getName();
- if (DOWNSAMPLING_CONSTANTS.contains(name)) {
- return "DownsamplingType." + name;
- }
- if (KNOWN_TYPES.contains(name)) {
- return name;
- }
- if (name.equals("this")) {
- return "this";
- }
- // Sample family lookup
- return "samples.getOrDefault(\"" + escapeJava(name) + "\", SampleFamily.EMPTY)";
- }
-
- private String visitConstant(final ConstantExpression expr) {
- final Object value = expr.getValue();
- if (value == null) {
- return "null";
- }
- if (value instanceof String) {
- return "\"" + escapeJava((String) value) + "\"";
- }
- if (value instanceof Integer) {
- return value.toString();
- }
- if (value instanceof Long) {
- return value + "L";
- }
- if (value instanceof Double) {
- return value.toString();
- }
- if (value instanceof Float) {
- return value + "f";
- }
- if (value instanceof Boolean) {
- return value.toString();
- }
- return value.toString();
- }
-
- // ---- MethodCall, Property, List ----
-
- private String visitMethodCall(final MethodCallExpression expr) {
- final String methodName = expr.getMethodAsString();
- final Expression objExpr = expr.getObjectExpression();
- final ArgumentListExpression args = toArgList(expr.getArguments());
-
- // tag(closure) -> TagFunction lambda
- if ("tag".equals(methodName) && args.getExpressions().size() == 1
- && args.getExpression(0) instanceof ClosureExpression) {
- final String obj = visitExpression(objExpr);
- final String lambda = visitTagClosure((ClosureExpression) args.getExpression(0));
- return obj + ".tag((TagFunction) " + lambda + ")";
- }
-
- // forEach(list, closure) -> ForEachFunction lambda
- if ("forEach".equals(methodName) && args.getExpressions().size() == 2
- && args.getExpression(1) instanceof ClosureExpression) {
- final String obj = visitExpression(objExpr);
- final String list = visitExpression(args.getExpression(0));
- final String lambda = visitForEachClosure((ClosureExpression) args.getExpression(1));
- return obj + ".forEach(" + list + ", (ForEachFunction) " + lambda + ")";
- }
-
- // instance(..., closure) -> last arg is PropertiesExtractor lambda
- if ("instance".equals(methodName) && !args.getExpressions().isEmpty()) {
- final Expression lastArg = args.getExpression(args.getExpressions().size() - 1);
- if (lastArg instanceof ClosureExpression) {
- final String obj = visitExpression(objExpr);
- final List<String> argStrs = new ArrayList<>();
- for (int i = 0; i < args.getExpressions().size() - 1; i++) {
- argStrs.add(visitExpression(args.getExpression(i)));
- }
- final String lambda = visitPropertiesExtractorClosure((ClosureExpression) lastArg);
- argStrs.add("(PropertiesExtractor) " + lambda);
- return obj + ".instance(" + String.join(", ", argStrs) + ")";
- }
- }
-
- final String obj = visitExpression(objExpr);
-
- // Static method calls: ClassExpression.method(...)
- if (objExpr instanceof ClassExpression) {
- final String typeName = objExpr.getType().getNameWithoutPackage();
- final List<String> argStrs = visitArgList(args);
- return typeName + "." + methodName + "(" + String.join(", ", argStrs) + ")";
- }
-
- // Regular instance method call: obj.method(args)
- final List<String> argStrs = visitArgList(args);
- return obj + "." + methodName + "(" + String.join(", ", argStrs) + ")";
- }
-
- private String visitProperty(final PropertyExpression expr) {
- final Expression obj = expr.getObjectExpression();
- final String prop = expr.getPropertyAsString();
-
- if (obj instanceof ClassExpression) {
- return obj.getType().getNameWithoutPackage() + "." + prop;
- }
- if (obj instanceof VariableExpression) {
- final String varName = ((VariableExpression) obj).getName();
- if (KNOWN_TYPES.contains(varName)) {
- return varName + "." + prop;
- }
- }
-
- return visitExpression(obj) + "." + prop;
- }
-
- private String visitList(final ListExpression expr) {
- final List<String> elements = new ArrayList<>();
- for (Expression e : expr.getExpressions()) {
- elements.add(visitExpression(e));
- }
- return "List.of(" + String.join(", ", elements) + ")";
- }
-
- // ---- Binary Arithmetic ----
-
- private String visitBinary(final BinaryExpression expr) {
- final int opType = expr.getOperation().getType();
-
- if (isArithmetic(opType)) {
- return visitArithmetic(expr.getLeftExpression(), expr.getRightExpression(), opType);
- }
-
- throw new UnsupportedOperationException(
- "Unsupported binary operator (not yet implemented): "
- + expr.getOperation().getText() + " in " + expr.getText());
- }
-
- /**
- * Arithmetic with operand-swap logic per upstream ExpandoMetaClass:
- * <pre>
- * SF + SF -> left.plus(right)
- * SF - SF -> left.minus(right)
- * SF * SF -> left.multiply(right)
- * SF / SF -> left.div(right)
- * SF op N -> sf.op(N)
- * N + SF -> sf.plus(N) (swap)
- * N - SF -> sf.minus(N).negative()
- * N * SF -> sf.multiply(N) (swap)
- * N / SF -> sf.newValue(v -> N / v)
- * N op N -> plain arithmetic
- * </pre>
- */
- private String visitArithmetic(final Expression left, final Expression right, final int opType) {
- final boolean leftNum = isNumberLiteral(left);
- final boolean rightNum = isNumberLiteral(right);
- final String leftStr = visitExpression(left);
- final String rightStr = visitExpression(right);
-
- if (leftNum && rightNum) {
- return "(" + leftStr + " " + opSymbol(opType) + " " + rightStr + ")";
- }
-
- if (!leftNum && rightNum) {
- return leftStr + "." + opMethod(opType) + "(" + rightStr + ")";
- }
-
- if (leftNum && !rightNum) {
- switch (opType) {
- case Types.PLUS:
- return rightStr + ".plus(" + leftStr + ")";
- case Types.MINUS:
- return rightStr + ".minus(" + leftStr + ").negative()";
- case Types.MULTIPLY:
- return rightStr + ".multiply(" + leftStr + ")";
- case Types.DIVIDE:
- return rightStr + ".newValue(v -> " + leftStr + " / v)";
- default:
- break;
- }
- }
-
- // SF op SF
- return leftStr + "." + opMethod(opType) + "(" + rightStr + ")";
- }
-
- private boolean isNumberLiteral(final Expression expr) {
- if (expr instanceof ConstantExpression) {
- return ((ConstantExpression) expr).getValue() instanceof Number;
- }
- return false;
- }
-
- private boolean isArithmetic(final int opType) {
- return opType == Types.PLUS || opType == Types.MINUS
- || opType == Types.MULTIPLY || opType == Types.DIVIDE;
- }
-
- private String opMethod(final int opType) {
- switch (opType) {
- case Types.PLUS: return "plus";
- case Types.MINUS: return "minus";
- case Types.MULTIPLY: return "multiply";
- case Types.DIVIDE: return "div";
- default: return "???";
- }
- }
-
- private String opSymbol(final int opType) {
- switch (opType) {
- case Types.PLUS: return "+";
- case Types.MINUS: return "-";
- case Types.MULTIPLY: return "*";
- case Types.DIVIDE: return "/";
- default: return "?";
- }
- }
-
- // ---- tag() Closure ----
-
- private String visitTagClosure(final ClosureExpression closure) {
- final Parameter[] params = closure.getParameters();
- final String tagsVar = (params != null && params.length > 0) ? params[0].getName() : "tags";
- final List<Statement> stmts = getStatements(closure.getCode());
-
- final StringBuilder sb = new StringBuilder();
- sb.append("(").append(tagsVar).append(" -> {\n");
- for (Statement s : stmts) {
- sb.append(" ").append(visitTagStatement(s, tagsVar)).append("\n");
- }
- sb.append(" return ").append(tagsVar).append(";\n");
- sb.append(" })");
- return sb.toString();
- }
-
- private String visitTagStatement(final Statement stmt, final String tagsVar) {
- if (stmt instanceof ExpressionStatement) {
- return visitTagExpr(((ExpressionStatement) stmt).getExpression(), tagsVar) + ";";
- }
- if (stmt instanceof ReturnStatement) {
- return "return " + tagsVar + ";";
- }
- if (stmt instanceof IfStatement) {
- return visitTagIf((IfStatement) stmt, tagsVar);
- }
- throw new UnsupportedOperationException(
- "Unsupported tag closure statement: " + stmt.getClass().getSimpleName());
- }
-
- // ---- If/Else + Compound Conditions in tag() ----
-
- private String visitTagIf(final IfStatement ifStmt, final String tagsVar) {
- final String condition = visitTagCondition(ifStmt.getBooleanExpression().getExpression(), tagsVar);
- final List<Statement> ifBody = getStatements(ifStmt.getIfBlock());
- final Statement elseBlock = ifStmt.getElseBlock();
-
- final StringBuilder sb = new StringBuilder();
- sb.append("if (").append(condition).append(") {\n");
- for (Statement s : ifBody) {
- sb.append(" ").append(visitTagStatement(s, tagsVar)).append("\n");
- }
- sb.append(" }");
-
- if (elseBlock != null && !(elseBlock instanceof EmptyStatement)) {
- sb.append(" else {\n");
- final List<Statement> elseBody = getStatements(elseBlock);
- for (Statement s : elseBody) {
- sb.append(" ").append(visitTagStatement(s, tagsVar)).append("\n");
- }
- sb.append(" }");
- }
-
- return sb.toString();
- }
-
- private String visitTagCondition(final Expression expr, final String tagsVar) {
- if (expr instanceof BinaryExpression) {
- final BinaryExpression bin = (BinaryExpression) expr;
- final int opType = bin.getOperation().getType();
-
- if (opType == Types.COMPARE_EQUAL) {
- return visitTagEquals(bin.getLeftExpression(), bin.getRightExpression(), tagsVar, false);
- }
- if (opType == Types.COMPARE_NOT_EQUAL) {
- return visitTagEquals(bin.getLeftExpression(), bin.getRightExpression(), tagsVar, true);
- }
- if (opType == Types.LOGICAL_OR) {
- return visitTagCondition(bin.getLeftExpression(), tagsVar)
- + " || " + visitTagCondition(bin.getRightExpression(), tagsVar);
- }
- if (opType == Types.LOGICAL_AND) {
- return visitTagCondition(bin.getLeftExpression(), tagsVar)
- + " && " + visitTagCondition(bin.getRightExpression(), tagsVar);
- }
- }
- if (expr instanceof BooleanExpression) {
- return visitTagCondition(((BooleanExpression) expr).getExpression(), tagsVar);
- }
- return visitTagValue(expr, tagsVar);
- }
-
- private String visitTagEquals(final Expression left, final Expression right,
- final String tagsVar, final boolean negate) {
- if (isNullConstant(right)) {
- final String leftStr = visitTagValue(left, tagsVar);
- return negate ? leftStr + " != null" : leftStr + " == null";
- }
- if (isNullConstant(left)) {
- final String rightStr = visitTagValue(right, tagsVar);
- return negate ? rightStr + " != null" : rightStr + " == null";
- }
-
- final String leftStr = visitTagValue(left, tagsVar);
- final String rightStr = visitTagValue(right, tagsVar);
-
- if (right instanceof ConstantExpression && ((ConstantExpression) right).getValue() instanceof String) {
- final String result = rightStr + ".equals(" + leftStr + ")";
- return negate ? "!" + result : result;
- }
- if (left instanceof ConstantExpression && ((ConstantExpression) left).getValue() instanceof String) {
- final String result = leftStr + ".equals(" + rightStr + ")";
- return negate ? "!" + result : result;
- }
- final String result = "Objects.equals(" + leftStr + ", " + rightStr + ")";
- return negate ? "!" + result : result;
- }
-
- // ---- Filter Conditions ----
-
- private String visitFilterCondition(final Expression expr, final String tagsVar) {
- if (expr instanceof NotExpression) {
- final Expression inner = ((NotExpression) expr).getExpression();
- final String val = visitTagValue(inner, tagsVar);
- return "(" + val + " == null || " + val + ".isEmpty())";
- }
-
- if (expr instanceof BinaryExpression) {
- final BinaryExpression bin = (BinaryExpression) expr;
- final int opType = bin.getOperation().getType();
-
- if (opType == Types.COMPARE_EQUAL) {
- return visitTagEquals(bin.getLeftExpression(), bin.getRightExpression(), tagsVar, false);
- }
- if (opType == Types.COMPARE_NOT_EQUAL) {
- return visitTagEquals(bin.getLeftExpression(), bin.getRightExpression(), tagsVar, true);
- }
- if (opType == Types.LOGICAL_OR) {
- return visitFilterCondition(bin.getLeftExpression(), tagsVar)
- + " || " + visitFilterCondition(bin.getRightExpression(), tagsVar);
- }
- if (opType == Types.LOGICAL_AND) {
- return visitFilterCondition(bin.getLeftExpression(), tagsVar)
- + " && " + visitFilterCondition(bin.getRightExpression(), tagsVar);
- }
- if (opType == Types.KEYWORD_IN) {
- final String val = visitTagValue(bin.getLeftExpression(), tagsVar);
- final String list = visitTagValue(bin.getRightExpression(), tagsVar);
- return list + ".contains(" + val + ")";
- }
- }
-
- if (expr instanceof BooleanExpression) {
- return visitFilterCondition(((BooleanExpression) expr).getExpression(), tagsVar);
- }
-
- final String val = visitTagValue(expr, tagsVar);
- return "(" + val + " != null && !" + val + ".isEmpty())";
- }
-
- private String visitTagExpr(final Expression expr, final String tagsVar) {
- if (expr instanceof BinaryExpression) {
- final BinaryExpression bin = (BinaryExpression) expr;
- if (bin.getOperation().getType() == Types.ASSIGN) {
- return visitTagAssignment(bin.getLeftExpression(), bin.getRightExpression(), tagsVar);
- }
- }
- if (expr instanceof MethodCallExpression) {
- final MethodCallExpression mce = (MethodCallExpression) expr;
- if ("remove".equals(mce.getMethodAsString()) && isTagsVar(mce.getObjectExpression(), tagsVar)) {
- final ArgumentListExpression args = toArgList(mce.getArguments());
- return tagsVar + ".remove(" + visitTagValue(args.getExpression(0), tagsVar) + ")";
- }
- }
- return visitTagValue(expr, tagsVar);
- }
-
- private String visitTagAssignment(final Expression left, final Expression right, final String tagsVar) {
- final String val = visitTagValue(right, tagsVar);
-
- if (left instanceof PropertyExpression) {
- final PropertyExpression prop = (PropertyExpression) left;
- if (isTagsVar(prop.getObjectExpression(), tagsVar)) {
- return tagsVar + ".put(\"" + escapeJava(prop.getPropertyAsString()) + "\", " + val + ")";
- }
- }
- if (left instanceof BinaryExpression) {
- final BinaryExpression sub = (BinaryExpression) left;
- if (sub.getOperation().getType() == Types.LEFT_SQUARE_BRACKET
- && isTagsVar(sub.getLeftExpression(), tagsVar)) {
- final String key = visitTagValue(sub.getRightExpression(), tagsVar);
- return tagsVar + ".put(" + key + ", " + val + ")";
- }
- }
- throw new UnsupportedOperationException(
- "Unsupported tag assignment target: " + left.getClass().getSimpleName() + " = " + left.getText());
- }
-
- String visitTagValue(final Expression expr, final String tagsVar) {
- if (expr instanceof PropertyExpression) {
- final PropertyExpression prop = (PropertyExpression) expr;
- if (isTagsVar(prop.getObjectExpression(), tagsVar)) {
- return tagsVar + ".get(\"" + escapeJava(prop.getPropertyAsString()) + "\")";
- }
- return visitProperty(prop);
- }
- if (expr instanceof BinaryExpression) {
- final BinaryExpression bin = (BinaryExpression) expr;
- if (bin.getOperation().getType() == Types.LEFT_SQUARE_BRACKET
- && isTagsVar(bin.getLeftExpression(), tagsVar)) {
- return tagsVar + ".get(" + visitTagValue(bin.getRightExpression(), tagsVar) + ")";
- }
- if (bin.getOperation().getType() == Types.PLUS) {
- return visitTagValue(bin.getLeftExpression(), tagsVar)
- + " + " + visitTagValue(bin.getRightExpression(), tagsVar);
- }
- }
- // Elvis operator — must check BEFORE TernaryExpression since it extends it
- if (expr instanceof ElvisOperatorExpression) {
- final ElvisOperatorExpression elvis = (ElvisOperatorExpression) expr;
- final String val = visitTagValue(elvis.getTrueExpression(), tagsVar);
- final String defaultVal = visitTagValue(elvis.getFalseExpression(), tagsVar);
- return "(" + val + " != null ? " + val + " : " + defaultVal + ")";
- }
- if (expr instanceof TernaryExpression) {
- final TernaryExpression tern = (TernaryExpression) expr;
- final String cond = visitFilterCondition(tern.getBooleanExpression().getExpression(), tagsVar);
- final String trueVal = visitTagValue(tern.getTrueExpression(), tagsVar);
- final String falseVal = visitTagValue(tern.getFalseExpression(), tagsVar);
- return "(" + cond + " ? " + trueVal + " : " + falseVal + ")";
- }
- if (expr instanceof MethodCallExpression) {
- final MethodCallExpression mce = (MethodCallExpression) expr;
- final String obj = visitTagValue(mce.getObjectExpression(), tagsVar);
- final ArgumentListExpression args = toArgList(mce.getArguments());
- final List<String> argStrs = new ArrayList<>();
- for (Expression a : args.getExpressions()) {
- argStrs.add(visitTagValue(a, tagsVar));
- }
- final String call = obj + "." + mce.getMethodAsString() + "(" + String.join(", ", argStrs) + ")";
- if (mce.isSafe()) {
- return "(" + obj + " != null ? " + call + " : null)";
- }
- return call;
- }
- if (expr instanceof VariableExpression) {
- final String name = ((VariableExpression) expr).getName();
- if (name.equals(tagsVar)) {
- return tagsVar;
- }
- return name;
- }
- if (expr instanceof ConstantExpression) {
- return visitConstant((ConstantExpression) expr);
- }
- if (expr instanceof ListExpression) {
- return visitList((ListExpression) expr);
- }
- if (expr instanceof MapExpression) {
- final MapExpression map = (MapExpression) expr;
- final List<String> entries = new ArrayList<>();
- for (MapEntryExpression entry : map.getMapEntryExpressions()) {
- entries.add(visitTagValue(entry.getKeyExpression(), tagsVar));
- entries.add(visitTagValue(entry.getValueExpression(), tagsVar));
- }
- return "Map.of(" + String.join(", ", entries) + ")";
- }
- return visitExpression(expr);
- }
-
- private boolean isTagsVar(final Expression expr, final String tagsVar) {
- return expr instanceof VariableExpression
- && ((VariableExpression) expr).getName().equals(tagsVar);
- }
-
- private List<Statement> getStatements(final Statement stmt) {
- if (stmt instanceof BlockStatement) {
- return ((BlockStatement) stmt).getStatements();
- }
- return List.of(stmt);
- }
-
- // ---- forEach() Closure ----
-
- private String visitForEachClosure(final ClosureExpression closure) {
- final Parameter[] params = closure.getParameters();
- final String prefixVar = (params != null && params.length > 0) ? params[0].getName() : "prefix";
- final String tagsVar = (params != null && params.length > 1) ? params[1].getName() : "tags";
-
- final List<Statement> stmts = getStatements(closure.getCode());
-
- final StringBuilder sb = new StringBuilder();
- sb.append("(").append(prefixVar).append(", ").append(tagsVar).append(") -> {\n");
- for (Statement s : stmts) {
- sb.append(" ").append(visitForEachStatement(s, tagsVar)).append("\n");
- }
- sb.append(" }");
- return sb.toString();
- }
-
- private String visitForEachStatement(final Statement stmt, final String tagsVar) {
- if (stmt instanceof ExpressionStatement) {
- return visitForEachExpr(((ExpressionStatement) stmt).getExpression(), tagsVar) + ";";
- }
- if (stmt instanceof ReturnStatement) {
- return "return;";
- }
- if (stmt instanceof IfStatement) {
- return visitForEachIf((IfStatement) stmt, tagsVar);
- }
- throw new UnsupportedOperationException(
- "Unsupported forEach closure statement: " + stmt.getClass().getSimpleName());
- }
-
- private String visitForEachExpr(final Expression expr, final String tagsVar) {
- if (expr instanceof DeclarationExpression) {
- final DeclarationExpression decl = (DeclarationExpression) expr;
- final String typeName = decl.getVariableExpression().getType().getNameWithoutPackage();
- final String varName = decl.getVariableExpression().getName();
- final String init = visitTagValue(decl.getRightExpression(), tagsVar);
- return typeName + " " + varName + " = " + init;
- }
- if (expr instanceof BinaryExpression) {
- final BinaryExpression bin = (BinaryExpression) expr;
- if (bin.getOperation().getType() == Types.ASSIGN) {
- final Expression left = bin.getLeftExpression();
- if (isTagWrite(left, tagsVar)) {
- return visitTagAssignment(left, bin.getRightExpression(), tagsVar);
- }
- if (left instanceof VariableExpression) {
- return ((VariableExpression) left).getName()
- + " = " + visitTagValue(bin.getRightExpression(), tagsVar);
- }
- }
- }
- return visitTagExpr(expr, tagsVar);
- }
-
- private String visitForEachIf(final IfStatement ifStmt, final String tagsVar) {
- final String condition = visitTagCondition(ifStmt.getBooleanExpression().getExpression(), tagsVar);
- final List<Statement> ifBody = getStatements(ifStmt.getIfBlock());
- final Statement elseBlock = ifStmt.getElseBlock();
-
- final StringBuilder sb = new StringBuilder();
- sb.append("if (").append(condition).append(") {\n");
- for (Statement s : ifBody) {
- sb.append(" ").append(visitForEachStatement(s, tagsVar)).append("\n");
- }
- sb.append(" }");
-
- if (elseBlock instanceof IfStatement) {
- sb.append(" else ").append(visitForEachIf((IfStatement) elseBlock, tagsVar));
- } else if (elseBlock != null && !(elseBlock instanceof EmptyStatement)) {
- sb.append(" else {\n");
- for (Statement s : getStatements(elseBlock)) {
- sb.append(" ").append(visitForEachStatement(s, tagsVar)).append("\n");
- }
- sb.append(" }");
- }
-
- return sb.toString();
- }
-
- private boolean isTagWrite(final Expression left, final String tagsVar) {
- if (left instanceof PropertyExpression) {
- return isTagsVar(((PropertyExpression) left).getObjectExpression(), tagsVar);
- }
- if (left instanceof BinaryExpression) {
- final BinaryExpression sub = (BinaryExpression) left;
- return sub.getOperation().getType() == Types.LEFT_SQUARE_BRACKET
- && isTagsVar(sub.getLeftExpression(), tagsVar);
- }
- return false;
- }
-
- private boolean isNullConstant(final Expression expr) {
- return expr instanceof ConstantExpression && ((ConstantExpression) expr).getValue() == null;
- }
-
- // ---- PropertiesExtractor Closure ----
-
- private String visitPropertiesExtractorClosure(final ClosureExpression closure) {
- final Parameter[] params = closure.getParameters();
- final String tagsVar = (params != null && params.length > 0) ? params[0].getName() : "tags";
- final List<Statement> stmts = getStatements(closure.getCode());
-
- final Statement last = stmts.get(stmts.size() - 1);
- Expression bodyExpr;
- if (last instanceof ExpressionStatement) {
- bodyExpr = ((ExpressionStatement) last).getExpression();
- } else if (last instanceof ReturnStatement) {
- bodyExpr = ((ReturnStatement) last).getExpression();
- } else {
- throw new UnsupportedOperationException(
- "Unsupported PropertiesExtractor closure body: " + last.getClass().getSimpleName());
- }
- return "(" + tagsVar + " -> " + visitTagValue(bodyExpr, tagsVar) + ")";
- }
-
- // ---- Batch Registration, Compilation, and Manifest Writing ----
-
- public void registerExpression(final String className, final String source) {
- expressionSources.put(className, source);
- }
-
- public void registerFilter(final String className, final String filterLiteral, final String source) {
- filterSources.put(className, source);
- filterLiteralToClass.put(filterLiteral, GENERATED_PACKAGE + "." + className);
- }
-
- /**
- * Compile all registered sources using javax.tools.JavaCompiler.
- *
- * @param sourceDir directory to write .java source files (package dirs created automatically)
- * @param outputDir directory for compiled .class files
- * @param classpath classpath for javac (semicolon/colon-separated JAR paths)
- * @throws IOException if file I/O fails
- */
- public void compileAll(final File sourceDir, final File outputDir,
- final String classpath) throws IOException {
- final Map<String, String> allSources = new LinkedHashMap<>();
- allSources.putAll(expressionSources);
- allSources.putAll(filterSources);
-
- if (allSources.isEmpty()) {
- log.info("No MAL sources to compile.");
- return;
- }
-
- final String packageDir = GENERATED_PACKAGE.replace('.', File.separatorChar);
- final File srcPkgDir = new File(sourceDir, packageDir);
- if (!srcPkgDir.exists() && !srcPkgDir.mkdirs()) {
- throw new IOException("Failed to create source dir: " + srcPkgDir);
- }
- if (!outputDir.exists() && !outputDir.mkdirs()) {
- throw new IOException("Failed to create output dir: " + outputDir);
- }
-
- final List<File> javaFiles = new ArrayList<>();
- for (Map.Entry<String, String> entry : allSources.entrySet()) {
- final File javaFile = new File(srcPkgDir, entry.getKey() + ".java");
- Files.writeString(javaFile.toPath(), entry.getValue());
- javaFiles.add(javaFile);
- }
-
- final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
- if (compiler == null) {
- throw new IllegalStateException("No Java compiler available — requires JDK");
- }
-
- final StringWriter errorWriter = new StringWriter();
-
- try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) {
- final Iterable<? extends JavaFileObject> compilationUnits =
- fileManager.getJavaFileObjectsFromFiles(javaFiles);
-
- final List<String> options = Arrays.asList(
- "-d", outputDir.getAbsolutePath(),
- "-classpath", classpath
- );
-
- final JavaCompiler.CompilationTask task = compiler.getTask(
- errorWriter, fileManager, null, options, null, compilationUnits);
-
- final boolean success = task.call();
- if (!success) {
- throw new RuntimeException(
- "Java compilation failed for " + javaFiles.size() + " MAL sources:\n"
- + errorWriter);
- }
- }
-
- log.info("Compiled {} MAL sources to {}", allSources.size(), outputDir);
- }
-
- /**
- * Write mal-expressions.txt manifest: one FQCN per line.
- */
- public void writeExpressionManifest(final File outputDir) throws IOException {
- final File manifestDir = new File(outputDir, "META-INF");
- if (!manifestDir.exists() && !manifestDir.mkdirs()) {
- throw new IOException("Failed to create META-INF dir: " + manifestDir);
- }
-
- final List<String> lines = expressionSources.keySet().stream()
- .map(name -> GENERATED_PACKAGE + "." + name)
- .collect(Collectors.toList());
- Files.write(new File(manifestDir, "mal-expressions.txt").toPath(), lines);
- log.info("Wrote mal-expressions.txt with {} entries", lines.size());
- }
-
- /**
- * Write mal-filter-expressions.properties manifest: literal=FQCN.
- */
- public void writeFilterManifest(final File outputDir) throws IOException {
- final File manifestDir = new File(outputDir, "META-INF");
- if (!manifestDir.exists() && !manifestDir.mkdirs()) {
- throw new IOException("Failed to create META-INF dir: " + manifestDir);
- }
-
- final List<String> lines = filterLiteralToClass.entrySet().stream()
- .map(e -> escapeProperties(e.getKey()) + "=" + e.getValue())
- .collect(Collectors.toList());
- Files.write(new File(manifestDir, "mal-filter-expressions.properties").toPath(), lines);
- log.info("Wrote mal-filter-expressions.properties with {} entries", lines.size());
- }
-
- private static String escapeProperties(final String s) {
- return s.replace("\\", "\\\\")
- .replace("=", "\\=")
- .replace(":", "\\:")
- .replace(" ", "\\ ");
- }
-
- // ---- Argument Utilities ----
-
- private ArgumentListExpression toArgList(final Expression args) {
- if (args instanceof ArgumentListExpression) {
- return (ArgumentListExpression) args;
- }
- if (args instanceof TupleExpression) {
- final ArgumentListExpression ale = new ArgumentListExpression();
- for (Expression e : ((TupleExpression) args).getExpressions()) {
- ale.addExpression(e);
- }
- return ale;
- }
- final ArgumentListExpression ale = new ArgumentListExpression();
- ale.addExpression(args);
- return ale;
- }
-
- private List<String> visitArgList(final ArgumentListExpression args) {
- final List<String> result = new ArrayList<>();
- for (Expression arg : args.getExpressions()) {
- result.add(visitExpression(arg));
- }
- return result;
- }
-
- // ---- Utility ----
-
- public static String escapeJava(final String s) {
- return s.replace("\\", "\\\\")
- .replace("\"", "\\\"")
- .replace("\n", "\\n")
- .replace("\r", "\\r")
- .replace("\t", "\\t");
- }
-}
diff --git a/oap-server/analyzer/mal-transpiler/src/test/java/org/apache/skywalking/oap/server/transpiler/mal/MalToJavaTranspilerTest.java b/oap-server/analyzer/mal-transpiler/src/test/java/org/apache/skywalking/oap/server/transpiler/mal/MalToJavaTranspilerTest.java
deleted file mode 100644
index 0207d8df32..0000000000
--- a/oap-server/analyzer/mal-transpiler/src/test/java/org/apache/skywalking/oap/server/transpiler/mal/MalToJavaTranspilerTest.java
+++ /dev/null
@@ -1,904 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.skywalking.oap.server.transpiler.mal;
-
-import java.io.File;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.List;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
-
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-class MalToJavaTranspilerTest {
-
- private MalToJavaTranspiler transpiler;
-
- @BeforeEach
- void setUp() {
- transpiler = new MalToJavaTranspiler();
- }
-
- // ---- AST Parsing + Simple Variable References ----
-
- @Test
- void simpleVariableReference() {
- final String java = transpiler.transpileExpression("MalExpr_test", "metric_name");
- assertNotNull(java);
-
- assertTrue(java.contains("package " + MalToJavaTranspiler.GENERATED_PACKAGE),
- "Should have correct package");
- assertTrue(java.contains("public class MalExpr_test implements MalExpression"),
- "Should implement MalExpression");
- assertTrue(java.contains("public SampleFamily run(Map<String, SampleFamily> samples)"),
- "Should have run method");
-
- assertTrue(java.contains("ctx.getSamples().add(\"metric_name\")"),
- "Should track sample name in parsing context");
-
- assertTrue(java.contains("samples.getOrDefault(\"metric_name\", SampleFamily.EMPTY)"),
- "Should look up sample family from map");
- }
-
- @Test
- void downsamplingConstantNotTrackedAsSample() {
- final String java = transpiler.transpileExpression("MalExpr_test", "SUM");
- assertNotNull(java);
-
- assertTrue(!java.contains("ctx.getSamples().add(\"SUM\")"),
- "Should not track DownsamplingType constant as sample");
-
- assertTrue(java.contains("DownsamplingType.SUM"),
- "Should resolve to DownsamplingType.SUM");
- }
-
- @Test
- void parseToAST_producesModuleNode() {
- final var ast = transpiler.parseToAST("some_metric");
- assertNotNull(ast, "Should produce a ModuleNode");
- assertNotNull(ast.getStatementBlock(), "Should have a statement block");
- }
-
- @Test
- void constantString() {
- final String java = transpiler.transpileExpression("MalExpr_test", "'hello'");
- assertNotNull(java);
- assertTrue(java.contains("\"hello\""),
- "Should convert Groovy string to Java string");
- }
-
- @Test
- void constantNumber() {
- final String java = transpiler.transpileExpression("MalExpr_test", "42");
- assertNotNull(java);
- assertTrue(java.contains("42"),
- "Should preserve number literal");
- }
-
- // ---- Method Chains + List Literals + Enum Properties ----
-
- @Test
- void simpleMethodChain() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric_name.sum(['a', 'b']).service(['svc'], Layer.GENERAL)");
- assertNotNull(java);
-
- assertTrue(java.contains(".sum(List.of(\"a\", \"b\"))"),
- "Should translate ['a','b'] to List.of(\"a\", \"b\")");
- assertTrue(java.contains(".service(List.of(\"svc\"), Layer.GENERAL)"),
- "Should translate Layer.GENERAL as enum");
- }
-
- @Test
- void tagEqualChain() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "cpu_seconds.tagNotEqual('mode', 'idle').sum(['host']).rate('PT1M')");
- assertNotNull(java);
-
- assertTrue(java.contains(".tagNotEqual(\"mode\", \"idle\")"),
- "Should translate tagNotEqual with string args");
- assertTrue(java.contains(".sum(List.of(\"host\"))"),
- "Should translate single-element list");
- assertTrue(java.contains(".rate(\"PT1M\")"),
- "Should translate rate with string arg");
- }
-
- @Test
- void downsamplingMethod() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.downsampling(SUM)");
- assertNotNull(java);
-
- assertTrue(java.contains(".downsampling(DownsamplingType.SUM)"),
- "Should resolve SUM to DownsamplingType.SUM");
- }
-
- @Test
- void retagByK8sMeta() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.retagByK8sMeta('service', K8sRetagType.Pod2Service, 'pod', 'namespace')");
- assertNotNull(java);
-
- assertTrue(java.contains(".retagByK8sMeta(\"service\", K8sRetagType.Pod2Service, \"pod\", \"namespace\")"),
- "Should translate K8sRetagType enum and string args");
- }
-
- @Test
- void histogramPercentile() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.sum(['le', 'svc']).histogram().histogram_percentile([50, 75, 90])");
- assertNotNull(java);
-
- assertTrue(java.contains(".histogram()"),
- "Should translate no-arg histogram()");
- assertTrue(java.contains(".histogram_percentile(List.of(50, 75, 90))"),
- "Should translate integer list");
- }
-
- @Test
- void sampleNameCollectionThroughChain() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "my_metric.sum(['a']).service(['svc'], Layer.GENERAL)");
- assertNotNull(java);
-
- assertTrue(java.contains("ctx.getSamples().add(\"my_metric\")"),
- "Should collect sample name from root of method chain");
- assertTrue(!java.contains("ctx.getSamples().add(\"a\")"),
- "Should NOT collect 'a' (it's a string constant arg, not a sample)");
- }
-
- @Test
- void detectPointEnum() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.serviceRelation(DetectPoint.CLIENT, ['src'], ['dst'], Layer.MESH_DP)");
- assertNotNull(java);
-
- assertTrue(java.contains("DetectPoint.CLIENT"),
- "Should translate DetectPoint enum");
- assertTrue(java.contains("Layer.MESH_DP"),
- "Should translate Layer enum");
- }
-
- @Test
- void enumImportsPresent() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.service(['svc'], Layer.GENERAL)");
- assertNotNull(java);
-
- assertTrue(java.contains("import org.apache.skywalking.oap.server.core.analysis.Layer;"),
- "Should import Layer");
- assertTrue(java.contains("import org.apache.skywalking.oap.server.core.source.DetectPoint;"),
- "Should import DetectPoint");
- assertTrue(java.contains("import org.apache.skywalking.oap.meter.analyzer.dsl.tagOpt.K8sRetagType;"),
- "Should import K8sRetagType");
- }
-
- // ---- Binary Arithmetic with Operand-Swap ----
-
- @Test
- void sfTimesNumber() {
- final String java = transpiler.transpileExpression("MalExpr_test", "metric * 100");
- assertNotNull(java);
- assertTrue(java.contains(".multiply(100)"),
- "SF * N should call .multiply(N)");
- }
-
- @Test
- void sfDivNumber() {
- final String java = transpiler.transpileExpression("MalExpr_test", "metric / 1024");
- assertNotNull(java);
- assertTrue(java.contains(".div(1024)"),
- "SF / N should call .div(N)");
- }
-
- @Test
- void numberMinusSf() {
- final String java = transpiler.transpileExpression("MalExpr_test", "100 - metric");
- assertNotNull(java);
- assertTrue(java.contains(".minus(100).negative()"),
- "N - SF should produce sf.minus(N).negative()");
- }
-
- @Test
- void numberDivSf() {
- final String java = transpiler.transpileExpression("MalExpr_test", "1 / metric");
- assertNotNull(java);
- assertTrue(java.contains(".newValue(v -> 1 / v)"),
- "N / SF should produce sf.newValue(v -> N / v)");
- }
-
- @Test
- void numberPlusSf() {
- final String java = transpiler.transpileExpression("MalExpr_test", "10 + metric");
- assertNotNull(java);
- assertTrue(java.contains(".plus(10)"),
- "N + SF should swap to sf.plus(N)");
- }
-
- @Test
- void numberTimesSf() {
- final String java = transpiler.transpileExpression("MalExpr_test", "100 * metric");
- assertNotNull(java);
- assertTrue(java.contains(".multiply(100)"),
- "N * SF should swap to sf.multiply(N)");
- }
-
- @Test
- void sfMinusSf() {
- final String java = transpiler.transpileExpression("MalExpr_test", "mem_total - mem_avail");
- assertNotNull(java);
- assertTrue(java.contains("ctx.getSamples().add(\"mem_total\")"),
- "Should collect both sample names");
- assertTrue(java.contains("ctx.getSamples().add(\"mem_avail\")"),
- "Should collect both sample names");
- assertTrue(java.contains(".minus("),
- "SF - SF should call .minus()");
- }
-
- @Test
- void sfDivSfTimesNumber() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "used_bytes / max_bytes * 100");
- assertNotNull(java);
- assertTrue(java.contains(".div("),
- "Should have .div() for SF / SF");
- assertTrue(java.contains(".multiply(100)"),
- "Should have .multiply(100) for result * 100");
- }
-
- @Test
- void nestedParenArithmetic() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "100 - ((mem_free * 100) / mem_total)");
- assertNotNull(java);
- assertTrue(java.contains(".multiply(100)"),
- "Should have inner multiply");
- assertTrue(java.contains(".negative()"),
- "100 - SF should produce .negative()");
- }
-
- @Test
- void parenthesizedWithMethodChain() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "(metric * 100).tagNotEqual('mode', 'idle').sum(['host']).rate('PT1M')");
- assertNotNull(java);
- assertTrue(java.contains(".multiply(100)"),
- "Should have multiply inside parens");
- assertTrue(java.contains(".tagNotEqual(\"mode\", \"idle\")"),
- "Should chain tagNotEqual after parens");
- assertTrue(java.contains(".rate(\"PT1M\")"),
- "Should chain rate at the end");
- }
-
- // ---- tag() Closure — Simple Cases ----
-
- @Test
- void tagAssignmentWithStringConcat() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.tag({tags -> tags.route = 'route/' + tags['route']})");
- assertNotNull(java);
-
- assertTrue(java.contains(".tag((TagFunction)"),
- "Should cast closure to TagFunction");
- assertTrue(java.contains("tags.put(\"route\", \"route/\" + tags.get(\"route\"))"),
- "Should translate assignment with string concat and subscript read");
- assertTrue(java.contains("return tags;"),
- "Should return tags at end of lambda");
- }
-
- @Test
- void tagRemove() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.tag({tags -> tags.remove('condition')})");
- assertNotNull(java);
-
- assertTrue(java.contains("tags.remove(\"condition\")"),
- "Should translate remove call");
- }
-
- @Test
- void tagPropertyToProperty() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.tag({tags -> tags.rs_nm = tags.set})");
- assertNotNull(java);
-
- assertTrue(java.contains("tags.put(\"rs_nm\", tags.get(\"set\"))"),
- "Should translate property read on RHS to tags.get()");
- }
-
- @Test
- void tagStringConcatWithPropertyRead() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.tag({tags -> tags.cluster = 'es::' + tags.cluster})");
- assertNotNull(java);
-
- assertTrue(java.contains("tags.put(\"cluster\", \"es::\" + tags.get(\"cluster\"))"),
- "Should translate string concat with property read");
- }
-
- @Test
- void tagClosureLambdaStructure() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.tag({tags -> tags.x = 'y'})");
- assertNotNull(java);
-
- assertTrue(java.contains("(tags -> {"),
- "Should have lambda opening");
- assertTrue(java.contains("return tags;"),
- "Should return tags variable");
- }
-
- @Test
- void tagWithSubscriptWrite() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.tag({tags -> tags['service_name'] = tags['svc']})");
- assertNotNull(java);
-
- assertTrue(java.contains("tags.put(\"service_name\", tags.get(\"svc\"))"),
- "Should translate subscript write and read");
- }
-
- @Test
- void tagChainAfterTag() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.tag({tags -> tags.x = 'y'}).sum(['host']).service(['svc'], Layer.GENERAL)");
- assertNotNull(java);
-
- assertTrue(java.contains(".tag((TagFunction)"),
- "Should have tag call");
- assertTrue(java.contains(".sum(List.of(\"host\"))"),
- "Should chain sum after tag");
- assertTrue(java.contains(".service(List.of(\"svc\"), Layer.GENERAL)"),
- "Should chain service after sum");
- }
-
- // ---- tag() Closure — if/else + Compound Conditions ----
-
- @Test
- void ifOnlyWithChainedOr() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.tag({tags -> if (tags['gc'] == 'PS Scavenge' || tags['gc'] == 'Copy' || tags['gc'] == 'ParNew' || tags['gc'] == 'G1 Young Generation') {tags.gc = 'young_gc_count'} })");
- assertNotNull(java);
-
- assertTrue(java.contains("if (\"PS Scavenge\".equals(tags.get(\"gc\"))"),
- "Should translate first == with constant on left for null-safety");
- assertTrue(java.contains("|| \"Copy\".equals(tags.get(\"gc\"))"),
- "Should chain || for second comparison");
- assertTrue(java.contains("|| \"ParNew\".equals(tags.get(\"gc\"))"),
- "Should chain || for third comparison");
- assertTrue(java.contains("|| \"G1 Young Generation\".equals(tags.get(\"gc\"))"),
- "Should chain || for fourth comparison");
- assertTrue(java.contains("tags.put(\"gc\", \"young_gc_count\")"),
- "Should translate assignment in if body");
- }
-
- @Test
- void ifElse() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.tag({tags -> if (tags['primary'] == 'true') {tags.primary = 'primary'} else {tags.primary = 'replica'} })");
- assertNotNull(java);
-
- assertTrue(java.contains("if (\"true\".equals(tags.get(\"primary\"))"),
- "Should translate == comparison in condition");
- assertTrue(java.contains("tags.put(\"primary\", \"primary\")"),
- "Should translate if-branch assignment");
- assertTrue(java.contains("} else {"),
- "Should have else clause");
- assertTrue(java.contains("tags.put(\"primary\", \"replica\")"),
- "Should translate else-branch assignment");
- }
-
- @Test
- void ifOnlyNoElse() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.tag({tags -> if (tags['level'] == '1') {tags.level = 'L1 aggregation'} })");
- assertNotNull(java);
-
- assertTrue(java.contains("if (\"1\".equals(tags.get(\"level\"))"),
- "Should translate condition");
- assertTrue(java.contains("tags.put(\"level\", \"L1 aggregation\")"),
- "Should translate if-body");
- assertTrue(!java.contains("else"),
- "Should NOT have else clause");
- }
-
- @Test
- void notEqualComparison() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.tag({tags -> if (tags['status'] != 'ok') {tags.status = 'error'} })");
- assertNotNull(java);
-
- assertTrue(java.contains("!\"ok\".equals(tags.get(\"status\"))"),
- "Should translate != with negated .equals()");
- }
-
- @Test
- void logicalAndCondition() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.tag({tags -> if (tags['a'] == 'x' && tags['b'] == 'y') {tags.c = 'z'} })");
- assertNotNull(java);
-
- assertTrue(java.contains("\"x\".equals(tags.get(\"a\")) && \"y\".equals(tags.get(\"b\"))"),
- "Should translate && with .equals() on both sides");
- }
-
- @Test
- void chainedTagClosuresWithIf() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.tag({tags -> if (tags['level'] == '1') {tags.level = 'L1 aggregation'} })" +
- ".tag({tags -> if (tags['level'] == '2') {tags.level = 'L2 aggregation'} })");
- assertNotNull(java);
-
- assertTrue(java.contains("\"1\".equals(tags.get(\"level\"))"),
- "Should translate first tag closure condition");
- assertTrue(java.contains("\"2\".equals(tags.get(\"level\"))"),
- "Should translate second tag closure condition");
- }
-
- @Test
- void ifWithMethodChainAfter() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.tag({tags -> if (tags['gc'] == 'Copy') {tags.gc = 'young'} }).sum(['host']).service(['svc'], Layer.GENERAL)");
- assertNotNull(java);
-
- assertTrue(java.contains(".tag((TagFunction)"),
- "Should have TagFunction cast");
- assertTrue(java.contains("\"Copy\".equals(tags.get(\"gc\"))"),
- "Should have if condition");
- assertTrue(java.contains(".sum(List.of(\"host\"))"),
- "Should chain sum after tag");
- }
-
- // ---- Filter Closures ----
-
- @Test
- void simpleEqualityFilter() {
- final String java = transpiler.transpileFilter("MalFilter_0",
- "{ tags -> tags.job_name == 'vm-monitoring' }");
- assertNotNull(java);
-
- assertTrue(java.contains("public class MalFilter_0 implements MalFilter"),
- "Should implement MalFilter");
- assertTrue(java.contains("public boolean test(Map<String, String> tags)"),
- "Should have test method");
- assertTrue(java.contains("\"vm-monitoring\".equals(tags.get(\"job_name\"))"),
- "Should translate == with constant on left");
- }
-
- @Test
- void filterPackageAndImports() {
- final String java = transpiler.transpileFilter("MalFilter_0",
- "{ tags -> tags.job_name == 'x' }");
- assertNotNull(java);
-
- assertTrue(java.contains("package " + MalToJavaTranspiler.GENERATED_PACKAGE),
- "Should have correct package");
- assertTrue(java.contains("import java.util.*;"),
- "Should import java.util");
- assertTrue(java.contains("import org.apache.skywalking.oap.meter.analyzer.dsl.*;"),
- "Should import dsl package");
- }
-
- @Test
- void orFilter() {
- final String java = transpiler.transpileFilter("MalFilter_1",
- "{ tags -> tags.job_name == 'flink-jobManager-monitoring' || tags.job_name == 'flink-taskManager-monitoring' }");
- assertNotNull(java);
-
- assertTrue(java.contains("\"flink-jobManager-monitoring\".equals(tags.get(\"job_name\"))"),
- "Should translate first ==");
- assertTrue(java.contains("|| \"flink-taskManager-monitoring\".equals(tags.get(\"job_name\"))"),
- "Should translate || with second ==");
- }
-
- @Test
- void inListFilter() {
- final String java = transpiler.transpileFilter("MalFilter_2",
- "{ tags -> tags.job_name in ['kubernetes-cadvisor', 'kube-state-metrics'] }");
- assertNotNull(java);
-
- assertTrue(java.contains("List.of(\"kubernetes-cadvisor\", \"kube-state-metrics\").contains(tags.get(\"job_name\"))"),
- "Should translate 'in' to List.of().contains()");
- }
-
- @Test
- void compoundAndFilter() {
- final String java = transpiler.transpileFilter("MalFilter_3",
- "{ tags -> tags.cloud_provider == 'aws' && tags.Namespace == 'AWS/S3' }");
- assertNotNull(java);
-
- assertTrue(java.contains("\"aws\".equals(tags.get(\"cloud_provider\"))"),
- "Should translate first ==");
- assertTrue(java.contains("&& \"AWS/S3\".equals(tags.get(\"Namespace\"))"),
- "Should translate && with second ==");
- }
-
- @Test
- void truthinessFilter() {
- final String java = transpiler.transpileFilter("MalFilter_4",
- "{ tags -> tags.cloud_provider == 'aws' && tags.Stage }");
- assertNotNull(java);
-
- assertTrue(java.contains("\"aws\".equals(tags.get(\"cloud_provider\"))"),
- "Should translate ==");
- assertTrue(java.contains("(tags.get(\"Stage\") != null && !tags.get(\"Stage\").isEmpty())"),
- "Should translate bare tags.Stage as truthiness check");
- }
-
- @Test
- void negatedTruthinessFilter() {
- final String java = transpiler.transpileFilter("MalFilter_5",
- "{ tags -> tags.cloud_provider == 'aws' && !tags.Method }");
- assertNotNull(java);
-
- assertTrue(java.contains("(tags.get(\"Method\") == null || tags.get(\"Method\").isEmpty())"),
- "Should translate !tags.Method as negated truthiness");
- }
-
- @Test
- void compoundWithTruthinessAndNegation() {
- final String java = transpiler.transpileFilter("MalFilter_6",
- "{ tags -> tags.cloud_provider == 'aws' && tags.Namespace == 'AWS/ApiGateway' && tags.Stage && !tags.Method }");
- assertNotNull(java);
-
- assertTrue(java.contains("\"aws\".equals(tags.get(\"cloud_provider\"))"),
- "Should translate first ==");
- assertTrue(java.contains("\"AWS/ApiGateway\".equals(tags.get(\"Namespace\"))"),
- "Should translate second ==");
- assertTrue(java.contains("(tags.get(\"Stage\") != null && !tags.get(\"Stage\").isEmpty())"),
- "Should translate truthiness");
- assertTrue(java.contains("(tags.get(\"Method\") == null || tags.get(\"Method\").isEmpty())"),
- "Should translate negated truthiness");
- }
-
- @Test
- void wrappedBlockFilter() {
- final String java = transpiler.transpileFilter("MalFilter_7",
- "{ tags -> {tags.cloud_provider == 'aws' && tags.Namespace == 'AWS/S3'} }");
- assertNotNull(java);
-
- assertTrue(java.contains("\"aws\".equals(tags.get(\"cloud_provider\"))"),
- "Should unwrap inner block and translate ==");
- assertTrue(java.contains("\"AWS/S3\".equals(tags.get(\"Namespace\"))"),
- "Should translate second == after unwrapping");
- }
-
- @Test
- void truthinessWithOrInParens() {
- final String java = transpiler.transpileFilter("MalFilter_8",
- "{ tags -> tags.cloud_provider == 'aws' && (tags.ApiId || tags.ApiName) }");
- assertNotNull(java);
-
- assertTrue(java.contains("(tags.get(\"ApiId\") != null && !tags.get(\"ApiId\").isEmpty())"),
- "Should translate ApiId truthiness");
- assertTrue(java.contains("(tags.get(\"ApiName\") != null && !tags.get(\"ApiName\").isEmpty())"),
- "Should translate ApiName truthiness");
- }
-
- // ---- forEach() Closure ----
-
- @Test
- void forEachBasicStructure() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.forEach(['client', 'server'], { prefix, tags -> tags[prefix + '_id'] = 'test' })");
- assertNotNull(java);
-
- assertTrue(java.contains(".forEach(List.of(\"client\", \"server\"), (ForEachFunction)"),
- "Should cast closure to ForEachFunction");
- assertTrue(java.contains("(prefix, tags) -> {"),
- "Should have two-parameter lambda");
- assertTrue(java.contains("tags.put(prefix + \"_id\", \"test\")"),
- "Should translate dynamic subscript write");
- }
-
- @Test
- void forEachNullCheckWithEarlyReturn() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.forEach(['client'], { prefix, tags -> if (tags[prefix + '_process_id'] != null) { return } })");
- assertNotNull(java);
-
- assertTrue(java.contains("tags.get(prefix + \"_process_id\") != null"),
- "Should translate null check");
- assertTrue(java.contains("return;"),
- "Should have void return for early exit");
- }
-
- @Test
- void forEachWithProcessRegistry() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.forEach(['client'], { prefix, tags -> " +
- "tags[prefix + '_process_id'] = ProcessRegistry.generateVirtualLocalProcess(tags.service, tags.instance) })");
- assertNotNull(java);
-
- assertTrue(java.contains("ProcessRegistry.generateVirtualLocalProcess(tags.get(\"service\"), tags.get(\"instance\"))"),
- "Should translate static method call with tag reads as args");
- assertTrue(java.contains("tags.put(prefix + \"_process_id\","),
- "Should translate dynamic subscript write");
- }
-
- @Test
- void forEachVarDeclaration() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.forEach(['component'], { key, tags -> String result = '' })");
- assertNotNull(java);
-
- assertTrue(java.contains("String result = \"\""),
- "Should translate variable declaration with empty string");
- }
-
- @Test
- void forEachVarDeclWithTagRead() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.forEach(['component'], { key, tags -> String protocol = tags['protocol'] })");
- assertNotNull(java);
-
- assertTrue(java.contains("String protocol = tags.get(\"protocol\")"),
- "Should translate var decl with tag read");
- }
-
- @Test
- void forEachIfElseIfChain() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.forEach(['component'], { key, tags -> " +
- "String protocol = tags['protocol']\n" +
- "String ssl = tags['is_ssl']\n" +
- "String result = ''\n" +
- "if (protocol == 'http' && ssl == 'true') { result = '129' } " +
- "else if (protocol == 'http') { result = '49' } " +
- "else if (ssl == 'true') { result = '130' } " +
- "else { result = '110' }\n" +
- "tags[key] = result })");
- assertNotNull(java);
-
- assertTrue(java.contains("String protocol = tags.get(\"protocol\")"),
- "Should declare protocol");
- assertTrue(java.contains("String ssl = tags.get(\"is_ssl\")"),
- "Should declare ssl");
-
- assertTrue(java.contains("\"http\".equals(protocol)"),
- "Should compare local var with .equals()");
- assertTrue(java.contains("\"true\".equals(ssl)"),
- "Should compare ssl with .equals()");
-
- assertTrue(java.contains("} else if ("),
- "Should produce else-if, not nested else { if }");
-
- assertTrue(java.contains("} else {"),
- "Should have final else");
- assertTrue(java.contains("result = \"110\""),
- "Should assign default value in else");
-
- assertTrue(java.contains("tags.put(key, result)"),
- "Should write result to tags[key]");
- }
-
- @Test
- void forEachLocalVarAssignment() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.forEach(['x'], { key, tags -> " +
- "String r = ''\n" +
- "r = 'abc'\n" +
- "tags[key] = r })");
- assertNotNull(java);
-
- assertTrue(java.contains("r = \"abc\""),
- "Should translate local var reassignment");
- }
-
- @Test
- void forEachEqualsOnStringComparison() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.forEach(['client'], { prefix, tags -> " +
- "if (tags[prefix + '_local'] == 'true') { tags[prefix + '_id'] = 'local' } })");
- assertNotNull(java);
-
- assertTrue(java.contains("\"true\".equals(tags.get(prefix + \"_local\"))"),
- "Should translate dynamic subscript comparison with .equals()");
- assertTrue(java.contains("tags.put(prefix + \"_id\", \"local\")"),
- "Should translate dynamic subscript assignment");
- }
-
- @Test
- void chainedForEach() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.forEach(['a'], { k1, tags -> tags[k1] = 'x' })" +
- ".forEach(['b'], { k2, tags -> tags[k2] = 'y' })");
- assertNotNull(java);
-
- assertTrue(java.contains("(ForEachFunction) (k1, tags)"),
- "Should have first forEach with k1");
- assertTrue(java.contains("(ForEachFunction) (k2, tags)"),
- "Should have second forEach with k2");
- }
-
- // ---- Elvis (?:), Safe Navigation (?.), Ternary (? :) ----
-
- @Test
- void safeNavigation() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.tag({tags -> tags.svc = tags['skywalking_service']?.trim() })");
- assertNotNull(java);
-
- assertTrue(java.contains("(tags.get(\"skywalking_service\") != null ? tags.get(\"skywalking_service\").trim() : null)"),
- "Should translate ?.trim() to null-checked call");
- }
-
- @Test
- void elvisOperator() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.tag({tags -> tags.svc = tags['name'] ?: 'unknown' })");
- assertNotNull(java);
-
- assertTrue(java.contains("(tags.get(\"name\") != null ? tags.get(\"name\") : \"unknown\")"),
- "Should translate ?: to null-check with default");
- }
-
- @Test
- void safeNavPlusElvis() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.tag({tags -> tags.service_name = 'APISIX::'+(tags['skywalking_service']?.trim()?:'APISIX') })");
- assertNotNull(java);
-
- assertTrue(java.contains("tags.get(\"skywalking_service\") != null ? tags.get(\"skywalking_service\").trim() : null"),
- "Should have safe nav for trim");
- assertTrue(java.contains("!= null ?") && java.contains(": \"APISIX\""),
- "Should have elvis default to APISIX");
- assertTrue(java.contains("\"APISIX::\" + "),
- "Should have string prefix concatenation");
- }
-
- @Test
- void ternaryOperator() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.tag({tags -> tags.service_name = tags.ApiId ? 'gw::'+tags.ApiId : 'gw::'+tags.ApiName })");
- assertNotNull(java);
-
- assertTrue(java.contains("tags.get(\"ApiId\") != null && !tags.get(\"ApiId\").isEmpty()"),
- "Should translate ternary condition as truthiness check");
- assertTrue(java.contains("\"gw::\" + tags.get(\"ApiId\")"),
- "Should have true branch expression");
- assertTrue(java.contains("\"gw::\" + tags.get(\"ApiName\")"),
- "Should have false branch expression");
- }
-
- @Test
- void safeNavInFilterCondition() {
- final String java = transpiler.transpileFilter("MalFilter_test",
- "{ tags -> tags.job_name == 'eks-monitoring' && tags.Service?.trim() }");
- assertNotNull(java);
-
- assertTrue(java.contains("\"eks-monitoring\".equals(tags.get(\"job_name\"))"),
- "Should translate == comparison");
- assertTrue(java.contains("tags.get(\"Service\") != null ? tags.get(\"Service\").trim() : null"),
- "Should have safe nav for Service?.trim()");
- }
-
- // ---- instance() with PropertiesExtractor, MapExpression ----
-
- @Test
- void instanceWithPropertiesExtractor() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.instance(['cluster', 'service'], '::', ['pod'], '', Layer.K8S_SERVICE, " +
- "{tags -> ['pod': tags.pod, 'namespace': tags.namespace]})");
- assertNotNull(java);
-
- assertTrue(java.contains(".instance("),
- "Should have instance call");
- assertTrue(java.contains("(PropertiesExtractor)"),
- "Should cast closure to PropertiesExtractor");
- assertTrue(java.contains("Map.of(\"pod\", tags.get(\"pod\"), \"namespace\", tags.get(\"namespace\"))"),
- "Should translate map literal to Map.of()");
- }
-
- @Test
- void mapExpressionInTagValue() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.instance(['svc'], ['inst'], Layer.GENERAL, {tags -> ['key': tags.val]})");
- assertNotNull(java);
-
- assertTrue(java.contains("Map.of(\"key\", tags.get(\"val\"))"),
- "Should translate single-entry map");
- assertTrue(java.contains("(PropertiesExtractor)"),
- "Should have PropertiesExtractor cast");
- }
-
- @Test
- void processRelationNoClosures() {
- final String java = transpiler.transpileExpression("MalExpr_test",
- "metric.processRelation('side', ['service'], ['instance'], " +
- "'client_process_id', 'server_process_id', 'component')");
- assertNotNull(java);
-
- assertTrue(java.contains(".processRelation(\"side\", List.of(\"service\"), List.of(\"instance\"), " +
- "\"client_process_id\", \"server_process_id\", \"component\")"),
- "Should translate processRelation as regular method call");
- }
-
- // ---- Compilation + Manifests ----
-
- @Test
- void sourceWrittenForCompilation(@TempDir Path tempDir) throws Exception {
- final String source = transpiler.transpileExpression("MalExpr_compile_test",
- "metric.sum(['host']).service(['svc'], Layer.GENERAL)");
- transpiler.registerExpression("MalExpr_compile_test", source);
-
- final File sourceDir = tempDir.resolve("src").toFile();
- final File outputDir = tempDir.resolve("classes").toFile();
-
- try {
- transpiler.compileAll(sourceDir, outputDir, System.getProperty("java.class.path"));
-
- final String classPath = MalToJavaTranspiler.GENERATED_PACKAGE.replace('.', File.separatorChar)
- + File.separator + "MalExpr_compile_test.class";
- assertTrue(new File(outputDir, classPath).exists(),
- "Compiled .class file should exist");
- } catch (RuntimeException e) {
- if (e.getMessage().contains("compilation failed")) {
- final String pkgPath = MalToJavaTranspiler.GENERATED_PACKAGE.replace('.', File.separatorChar);
- final File javaFile = new File(sourceDir, pkgPath + "/MalExpr_compile_test.java");
- assertTrue(javaFile.exists(), "Source .java file should be written");
- final String written = Files.readString(javaFile.toPath());
- assertTrue(written.contains("implements MalExpression"),
- "Written source should implement MalExpression");
- } else {
- throw e;
- }
- }
- }
-
- @Test
- void expressionManifest(@TempDir Path tempDir) throws Exception {
- transpiler.registerExpression("MalExpr_a",
- transpiler.transpileExpression("MalExpr_a", "metric_a"));
- transpiler.registerExpression("MalExpr_b",
- transpiler.transpileExpression("MalExpr_b", "metric_b"));
-
- final File outputDir = tempDir.toFile();
- transpiler.writeExpressionManifest(outputDir);
-
- final File manifest = new File(outputDir, "META-INF/mal-expressions.txt");
- assertTrue(manifest.exists(), "Manifest file should exist");
-
- final List<String> lines = Files.readAllLines(manifest.toPath());
- assertTrue(lines.contains(MalToJavaTranspiler.GENERATED_PACKAGE + ".MalExpr_a"),
- "Should contain MalExpr_a FQCN");
- assertTrue(lines.contains(MalToJavaTranspiler.GENERATED_PACKAGE + ".MalExpr_b"),
- "Should contain MalExpr_b FQCN");
- }
-
- @Test
- void filterManifest(@TempDir Path tempDir) throws Exception {
- final String literal = "{ tags -> tags.job == 'x' }";
- transpiler.registerFilter("MalFilter_0", literal,
- transpiler.transpileFilter("MalFilter_0", literal));
-
- final File outputDir = tempDir.toFile();
- transpiler.writeFilterManifest(outputDir);
-
- final File manifest = new File(outputDir, "META-INF/mal-filter-expressions.properties");
- assertTrue(manifest.exists(), "Filter manifest should exist");
-
- final String content = Files.readString(manifest.toPath());
- assertTrue(content.contains(MalToJavaTranspiler.GENERATED_PACKAGE + ".MalFilter_0"),
- "Should contain MalFilter_0 FQCN");
- }
-}
diff --git a/oap-server/analyzer/meter-analyzer-v2/pom.xml b/oap-server/analyzer/meter-analyzer-v2/pom.xml
deleted file mode 100644
index 8bbcaa43ed..0000000000
--- a/oap-server/analyzer/meter-analyzer-v2/pom.xml
+++ /dev/null
@@ -1,78 +0,0 @@
-<?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>
... 8272 lines suppressed ...