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/06 03:17:05 UTC

(skywalking) branch master updated: Introduce MAL/LAL/Hierarchy V2 engine — replace Groovy DSL runtime with ANTLR4 + Javassist (#13723)

This is an automated email from the ASF dual-hosted git repository.

wusheng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking.git


The following commit(s) were added to refs/heads/master by this push:
     new b106892f20 Introduce MAL/LAL/Hierarchy V2 engine — replace Groovy DSL runtime with ANTLR4 + Javassist (#13723)
b106892f20 is described below

commit b106892f20a1136713986278be17ecf53279831e
Author: 吴晟 Wu Sheng <wu...@foxmail.com>
AuthorDate: Fri Mar 6 11:16:59 2026 +0800

    Introduce MAL/LAL/Hierarchy V2 engine — replace Groovy DSL runtime with ANTLR4 + Javassist (#13723)
    
    Replace the Groovy-based DSL runtime for MAL (Meter Analysis Language), LAL
    (Log Analysis Language), and Hierarchy matching rules with compile-time ANTLR4
    parsing and Javassist bytecode generation — extending the same approach used
    by the OAL V2 engine (#13699) to all remaining DSL compilers.
    
    All three compilers follow the same pipeline:
    
      DSL string → ANTLR4 parse → Immutable AST → Javassist bytecode → Direct Java execution
    
    ## Why
    
    - Remove Groovy runtime dependency (~7 MB) from OAP server classpath
    - Eliminate runtime interpretation — generated bytecode uses direct method
      calls with zero reflection at runtime
    - Thread-safe by design — generated instances are stateless singletons;
      per-request state passed as explicit parameters (no ThreadLocal, no
      mutable wrappers)
    - Fail-fast at boot — DSL compilation errors caught during startup with
      file/line/column reporting, not at first log/metric arrival
    - Debugger-friendly — all generated methods include LocalVariableTable (LVT)
      entries with named variables
    
    ## Architecture
    
      | DSL       | Compiled Interface                    | Runtime Signature                            |
      |-----------|---------------------------------------|----------------------------------------------|
      | MAL       | MalExpression                         | SampleFamily run(Map<String, SampleFamily>)  |
      | LAL       | LalExpression                         | void execute(FilterSpec, ExecutionContext)    |
      | Hierarchy | BiFunction<Service, Service, Boolean> | Boolean apply(Service, Service)              |
    
    All v2 classes live under `.v2.` packages to avoid FQCN conflicts with v1
    (Groovy) classes. v1 code is preserved in test/script-cases/script-runtime-with-groovy/
    for cross-version comparison testing only — it is no longer used at runtime.
    
    ### MAL Compiler
    
    - ANTLR4 grammar: MALLexer.g4 (132 lines), MALParser.g4 (282 lines)
    - Parser: MALScriptParser.java (888 lines) → immutable MALExpressionModel AST
    - Codegen: MALClassGenerator.java (1,334 lines) + MALClosureCodegen.java (783 lines)
    - Closures compiled as methods on the main class, wrapped via LambdaMetafactory
      (same mechanism javac uses for lambdas — JIT-inlineable, no separate .class files)
    - Supports regex match (=~), def type inference, GString interpolation, time() scalar,
      decorate() bean mode, ternary expressions, unary minus, null arguments
    
    ### LAL Compiler
    
    - ANTLR4 grammar: LALLexer.g4 (175 lines), LALParser.g4 (476 lines)
    - Parser: LALScriptParser.java (947 lines) → immutable LALScriptModel AST
    - Codegen: LALClassGenerator.java (553 lines) + LALBlockCodegen.java (1,157 lines)
    - Single flat class per rule (no closures, no sub-classes)
    - LalRuntimeHelper: toStr() preserves null (unlike String.valueOf → "null"),
      mapVal() for JSON parsed map access, proto getter chain resolution
    - LALSourceTypeProvider SPI resolves extraLogType proto class at compile time;
      proto getter chains resolved via reflection at compile time, emitted as direct
      method calls with null-safe ternary chains at runtime
    
    ### Hierarchy Compiler
    
    - ANTLR4 grammar: HierarchyRuleLexer.g4 (105 lines), HierarchyRuleParser.g4 (135 lines)
    - Parser: HierarchyRuleScriptParser.java (375 lines) → immutable HierarchyRuleModel AST
    - Codegen: HierarchyRuleClassGenerator.java (543 lines)
    - CompiledHierarchyRuleProvider registered via SPI as HierarchyRuleProvider
    - Property access → getter methods, == → Objects.equals(), supports if/else/return blocks
    
    ## v1-v2 Cross-Version Verification
    
    Every production DSL expression is run through both Groovy v1 and Javassist v2,
    then asserted for identical results. Both v1 and v2 must pass — v1 failure also
    fail()s (no silent skip).
    
      | DSL                  | Expressions | Source                                      |
      |----------------------|-------------|---------------------------------------------|
      | MAL expressions      | 1,229       | 73 YAML files across 6 directories          |
      | MAL filter closures  | 31          | Separate MalFilterComparisonTest             |
      | LAL scripts          | 29          | test-lal/ directory                          |
      | Hierarchy rules      | 4 rules     | test-hierarchy-definition.data.yaml          |
      | Total                | ~1,290+     |                                              |
    
    MAL comparison validates: samples, scopeType, downsampling, isHistogram,
    scopeLabels, aggregationLabels, plus runtime Sample[] output arrays.
    LAL comparison validates: shouldAbort, shouldSave, log.service,
    log.serviceInstance, log.endpoint, log.layer, log.timestamp, log.tags.
    
    76 companion .data.yaml files provide realistic SampleFamily mock input.
    10 .input.data files provide proto-json LogData/extraLog mock data for LAL.
    
    ## Performance (JMH benchmarks)
    
      | DSL       | Operation | v2 speedup over v1 |
      |-----------|-----------|--------------------|
      | MAL       | execute   | ~6.8x faster       |
      | LAL       | compile   | ~39x faster        |
      | LAL       | execute   | ~2.8x faster       |
      | Hierarchy | execute   | ~2.6x faster       |
    
    ## Generated .class File Debugging
    
    Set SW_DYNAMIC_CLASS_ENGINE_DEBUG to any non-empty value to dump .class files:
    
      | DSL       | Output directory              |
      |-----------|-------------------------------|
      | OAL       | oal-rt-generated-classes/     |
      | MAL       | mal-generated-classes/        |
      | LAL       | lal-generated-classes/        |
      | Hierarchy | hierarchy-generated-classes/  |
    
    Renamed from SW_OAL_ENGINE_DEBUG → SW_DYNAMIC_CLASS_ENGINE_DEBUG to cover all
    four DSL compilers. Documentation: docs/en/operation/dynamic-code-generation-debugging.md
    
    ## New Maven Modules
    
    - oap-server/analyzer/hierarchy/ — Hierarchy rule v2 compiler + SPI provider
    - test/script-cases/script-runtime-with-groovy/ — Test aggregator with 6 sub-modules:
      mal-v1-with-groovy, lal-v1-with-groovy, hierarchy-v1-with-groovy (v1 runtimes),
      mal-lal-v1-v2-checker, hierarchy-v1-v2-checker (cross-version comparison tests)
    
    ## Other Changes
    
    - Remove initExp from MAL configuration — was internal Groovy startup validation;
      v2 compiler performs fail-fast validation at startup natively
    - Update hierarchy rule documentation — rules now use a dedicated expression grammar
      (property access, String methods, if/else, comparisons, logical operators) instead
      of Groovy scripts. All shipped rules are fully compatible.
    - Remove PowerMock dependency — replaced with ReflectUtil (standard Java reflection
      + sun.misc.Unsafe for final fields) to support JDK 25+
    - Upgrade Byte Buddy to 1.18.7 — configure explicit -javaagent for Mockito/Byte Buddy
      in Surefire to avoid JDK 25 dynamic agent loading warnings
    - Relocate JMH benchmarks from oap-server/microbench/ to target modules
    - Add CLAUDE.md AI assistant guides for meter-analyzer, log-analyzer, hierarchy, oal-rt
    - Add Claude Code skills: compile, test, license, gh-pull-request, ci-e2e-debug,
      run-e2e, generate-classes
    
    ## Documentation
    
    - docs/en/academy/dsl-compiler-design.md — Architecture overview of all 4 DSL compilers
    - docs/en/operation/dynamic-code-generation-debugging.md — .class dump guide
    - docs/en/changes/changes.md — Changelog entries
    - docs/en/setup/backend/configuration-vocabulary.md — SW_DYNAMIC_CLASS_ENGINE_DEBUG
    - Updated: lal.md, service-hierarchy.md, service-hierarchy-configuration.md,
      backend-meter.md, backend-zabbix.md, oal.md
    
    Reviewed-by: Gemini Code Review (concurrency, thread-safety, feature parity validated)
    Verified-by: Gemini Verification Report (100% script coverage, all branch paths exercised)
    Approved-by: wankai123
---
 .claude/skills/ci-e2e-debug/SKILL.md               |   41 +
 .claude/skills/compile/SKILL.md                    |  109 ++
 .claude/skills/generate-classes/SKILL.md           |   65 +
 .claude/skills/gh-pull-request/SKILL.md            |  112 ++
 .claude/skills/license/SKILL.md                    |   62 +
 .claude/skills/run-e2e/SKILL.md                    |  146 ++
 .claude/skills/test/SKILL.md                       |   81 ++
 .github/workflows/skywalking.yaml                  |    3 +-
 .gitignore                                         |   10 +-
 CLAUDE.md                                          |  395 +----
 dist-material/release-docs/LICENSE                 |    1 -
 docker/.env                                        |    2 +-
 docker/docker-compose.yml                          |    2 +-
 docs/en/academy/dsl-compiler-design.md             |  163 +++
 docs/en/changes/changes.md                         |   13 +
 docs/en/concepts-and-designs/lal.md                |   34 +-
 docs/en/concepts-and-designs/mal.md                |    2 -
 docs/en/concepts-and-designs/oal.md                |    3 +-
 .../service-hierarchy-configuration.md             |    2 +-
 docs/en/concepts-and-designs/service-hierarchy.md  |   46 +-
 docs/en/guides/claude-code-skills.md               |   29 +
 .../operation/dynamic-code-generation-debugging.md |  187 +++
 docs/en/setup/backend/backend-meter.md             |    2 -
 docs/en/setup/backend/backend-zabbix.md            |    2 -
 docs/en/setup/backend/configuration-vocabulary.md  |    2 +-
 docs/menu.yml                                      |    8 +
 oap-server/ai-pipeline/pom.xml                     |    5 +-
 .../src/test/java/BaselineServerTest.java          |    4 +-
 oap-server/analyzer/agent-analyzer/pom.xml         |    6 +
 .../provider/meter/config/MeterConfig.java         |    3 +-
 .../meter/process/MeterProcessService.java         |    2 +-
 .../provider/meter/process/MeterProcessor.java     |    8 +-
 .../provider/meter/process/SampleBuilder.java      |    2 +-
 .../provider/meter/process/MeterProcessorTest.java |    4 +-
 .../trace/TraceSamplingPolicyWatcherTest.java      |    6 +-
 .../trace/UninstrumentedGatewaysConfigTest.java    |    4 +-
 oap-server/analyzer/hierarchy/CLAUDE.md            |  140 ++
 .../analyzer/{meter-analyzer => hierarchy}/pom.xml |   35 +-
 .../hierarchy/rt/grammar/HierarchyRuleLexer.g4     |  105 ++
 .../hierarchy/rt/grammar/HierarchyRuleParser.g4    |  135 ++
 .../v2/compiler/CompiledHierarchyRuleProvider.java |   73 +
 .../v2/compiler/HierarchyRuleClassGenerator.java   |  543 +++++++
 .../config/v2/compiler/HierarchyRuleModel.java     |  260 ++++
 .../v2/compiler/HierarchyRuleScriptParser.java     |  375 +++++
 .../rule/rt/HierarchyRulePackageHolder.java}       |   10 +-
 ...erarchyDefinitionService$HierarchyRuleProvider} |    2 +-
 .../compiler/HierarchyRuleClassGeneratorTest.java  |  158 ++
 .../v2/compiler/HierarchyRuleScriptParserTest.java |  159 ++
 oap-server/analyzer/log-analyzer/CLAUDE.md         |  263 ++++
 oap-server/analyzer/log-analyzer/pom.xml           |   39 +-
 .../apache/skywalking/lal/rt/grammar/LALLexer.g4   |  175 +++
 .../apache/skywalking/lal/rt/grammar/LALParser.g4  |  476 ++++++
 .../log/analyzer/v2/compiler/LALBlockCodegen.java  | 1157 +++++++++++++++
 .../analyzer/v2/compiler/LALClassGenerator.java    |  553 +++++++
 .../log/analyzer/v2/compiler/LALCodegenHelper.java |  107 ++
 .../log/analyzer/v2/compiler/LALScriptModel.java   |  563 +++++++
 .../log/analyzer/v2/compiler/LALScriptParser.java  |  947 ++++++++++++
 .../compiler/rt/LalExpressionPackageHolder.java}   |   10 +-
 .../analyzer/v2/compiler/rt/LalRuntimeHelper.java  |  316 ++++
 .../skywalking/oap/log/analyzer/v2/dsl/DSL.java    |   90 ++
 .../Binding.java => v2/dsl/ExecutionContext.java}  |  124 +-
 .../oap/log/analyzer/v2/dsl/LalExpression.java}    |   29 +-
 .../log/analyzer/v2/dsl/spec/AbstractSpec.java}    |   19 +-
 .../{ => v2}/dsl/spec/extractor/ExtractorSpec.java |  245 ++--
 .../extractor/sampledtrace/SampledTraceSpec.java   |  100 ++
 .../dsl/spec/extractor/slowsql/SlowSqlSpec.java    |   30 +-
 .../analyzer/v2/dsl/spec/filter/FilterSpec.java    |  256 ++++
 .../dsl/spec/parser/AbstractParserSpec.java        |    6 +-
 .../{ => v2}/dsl/spec/parser/JsonParserSpec.java   |    4 +-
 .../{ => v2}/dsl/spec/parser/TextParserSpec.java   |   25 +-
 .../{ => v2}/dsl/spec/parser/YamlParserSpec.java   |    4 +-
 .../log/analyzer/v2/dsl/spec/sink/SamplerSpec.java |   68 +
 .../analyzer/{ => v2}/dsl/spec/sink/SinkSpec.java  |   33 +-
 .../dsl/spec/sink/sampler/PossibilitySampler.java  |    4 +-
 .../dsl/spec/sink/sampler/RateLimitingSampler.java |    2 +-
 .../{ => v2}/dsl/spec/sink/sampler/Sampler.java    |    2 +-
 .../{ => v2}/module/LogAnalyzerModule.java         |    4 +-
 .../log/analyzer/{ => v2}/provider/LALConfig.java  |   10 +-
 .../log/analyzer/{ => v2}/provider/LALConfigs.java |   10 +-
 .../{ => v2}/provider/LogAnalyzerModuleConfig.java |    6 +-
 .../provider/LogAnalyzerModuleProvider.java        |   12 +-
 .../provider/log/ILogAnalysisListenerManager.java  |    6 +-
 .../{ => v2}/provider/log/ILogAnalyzerService.java |    2 +-
 .../{ => v2}/provider/log/LogAnalyzer.java         |   25 +-
 .../provider/log/LogAnalyzerServiceImpl.java       |    8 +-
 .../provider/log/analyzer/LogAnalyzerFactory.java  |    2 +-
 .../provider/log/listener/LogAnalysisListener.java |    2 +-
 .../log/listener/LogAnalysisListenerFactory.java   |    2 +-
 .../provider/log/listener/LogFilterListener.java   |  155 ++
 .../provider/log/listener/LogSinkListener.java     |    2 +-
 .../log/listener/LogSinkListenerFactory.java       |    2 +-
 .../provider/log/listener/RecordSinkListener.java  |    4 +-
 .../provider/log/listener/TrafficSinkListener.java |    4 +-
 .../log/analyzer/v2/spi/LALSourceTypeProvider.java |   53 +
 ...ywalking.oap.server.library.module.ModuleDefine |    2 +-
 ...alking.oap.server.library.module.ModuleProvider |    2 +-
 .../v2/compiler/LALClassGeneratorTest.java         |  672 +++++++++
 .../v2/compiler/LALExpressionExecutionTest.java    |  585 ++++++++
 .../analyzer/v2/compiler/LALScriptParserTest.java  |  529 +++++++
 .../compiler/TestMeshLALSourceTypeProvider.java}   |   30 +-
 .../oap/log/analyzer/v2/dsl/DSLV2Test.java         |   49 +
 ....oap.log.analyzer.v2.spi.LALSourceTypeProvider} |    2 +-
 oap-server/analyzer/meter-analyzer/CLAUDE.md       |  162 +++
 oap-server/analyzer/meter-analyzer/pom.xml         |   32 +-
 .../apache/skywalking/mal/rt/grammar/MALLexer.g4   |  132 ++
 .../apache/skywalking/mal/rt/grammar/MALParser.g4  |  282 ++++
 .../oap/meter/analyzer/{ => v2}/Analyzer.java      |   83 +-
 .../oap/meter/analyzer/{ => v2}/MetricConvert.java |   83 +-
 .../meter/analyzer/{ => v2}/MetricRuleConfig.java  |   12 +-
 .../analyzer/v2/compiler/MALClassGenerator.java    | 1334 +++++++++++++++++
 .../analyzer/v2/compiler/MALClosureCodegen.java    |  783 ++++++++++
 .../analyzer/v2/compiler/MALCodegenHelper.java     |  318 ++++
 .../analyzer/v2/compiler/MALExpressionModel.java   |  638 ++++++++
 .../analyzer/v2/compiler/MALScriptParser.java      |  888 +++++++++++
 .../compiler/rt/MalExpressionPackageHolder.java}   |   10 +-
 .../analyzer/v2/compiler/rt/MalRuntimeHelper.java  |   76 +
 .../skywalking/oap/meter/analyzer/v2/dsl/DSL.java  |   65 +
 .../analyzer/{ => v2}/dsl/DownsamplingType.java    |    2 +-
 .../oap/meter/analyzer/v2/dsl/Expression.java      |   95 ++
 .../meter/analyzer/v2/dsl/ExpressionMetadata.java  |   66 +
 .../{ => v2}/dsl/ExpressionParsingException.java   |    2 +-
 .../analyzer/{ => v2}/dsl/FilterExpression.java    |   30 +-
 .../dsl/MalExpression.java}                        |   18 +-
 .../dsl/MalFilter.java}                            |   11 +-
 .../oap/meter/analyzer/{ => v2}/dsl/Result.java    |   12 +-
 .../oap/meter/analyzer/{ => v2}/dsl/Sample.java    |    4 +-
 .../meter/analyzer/{ => v2}/dsl/SampleFamily.java  |  119 +-
 .../analyzer/{ => v2}/dsl/SampleFamilyBuilder.java |    2 +-
 .../analyzer/v2/dsl/SampleFamilyFunctions.java     |   70 +
 .../{ => v2}/dsl/counter/CounterWindow.java        |    2 +-
 .../meter/analyzer/{ => v2}/dsl/counter/ID.java    |    2 +-
 .../dsl/entity}/EndpointEntityDescription.java     |    2 +-
 .../dsl/entity}/EntityDescription.java             |    2 +-
 .../dsl/entity}/InstanceEntityDescription.java     |    6 +-
 .../dsl/entity}/ProcessEntityDescription.java      |    2 +-
 .../entity}/ProcessRelationEntityDescription.java  |    2 +-
 .../dsl/entity}/ServiceEntityDescription.java      |    2 +-
 .../entity}/ServiceRelationEntityDescription.java  |    2 +-
 .../{ => v2}/dsl/registry/ProcessRegistry.java     |    4 +-
 .../analyzer/{ => v2}/dsl/tagOpt/K8sRetagType.java |    6 +-
 .../meter/analyzer/{ => v2}/dsl/tagOpt/Retag.java  |    4 +-
 .../analyzer/{ => v2}/k8s/K8sInfoRegistry.java     |    2 +-
 .../prometheus/PrometheusMetricConverter.java      |   10 +-
 .../{ => v2}/prometheus/rule/MetricsRule.java      |    4 +-
 .../analyzer/{ => v2}/prometheus/rule/Rule.java    |   10 +-
 .../analyzer/{ => v2}/prometheus/rule/Rules.java   |    2 +-
 .../v2/compiler/MALClassGeneratorTest.java         |  453 ++++++
 .../analyzer/v2/compiler/MALScriptParserTest.java  |  406 ++++++
 .../oap/meter/analyzer/v2/dsl/DSLV2Test.java       |   92 ++
 oap-server/analyzer/pom.xml                        |    5 +-
 oap-server/exporter/pom.xml                        |    6 +
 .../provider/grpc/GRPCExporterProviderTest.java    |    4 +-
 .../exporter/provider/grpc/GRPCExporterTest.java   |   10 +-
 oap-server/microbench/pom.xml                      |  105 --
 .../microbench/base/AbstractMicrobenchmark.java    |  116 --
 .../openapi/EndpointGrouping4OpenapiBenchmark.java |  138 --
 .../group/uri/RegexVSQuickMatchBenchmark.java      |  195 ---
 .../ebpf/EBPFProfilingAnalyzerBenchmark.java       |  409 ------
 .../library/util/StringFormatGroupBenchmark.java   |  124 --
 oap-server/oal-rt/CLAUDE.md                        |    2 +-
 .../oal/v2/generator/OALClassGeneratorV2.java      |  125 +-
 oap-server/pom.xml                                 |    9 -
 oap-server/server-alarm-plugin/pom.xml             |    6 +
 .../server/core/alarm/provider/AlarmCoreTest.java  |    4 +-
 .../alarm/provider/AlarmModuleProviderTest.java    |    4 +-
 .../core/alarm/provider/NotifyHandlerTest.java     |    4 +-
 .../core/alarm/provider/RunningRuleTest.java       |   10 +-
 .../cluster-consul-plugin/pom.xml                  |    6 +
 .../ClusterModuleConsulProviderFunctionalIT.java   |    6 +-
 .../consul/ClusterModuleConsulProviderTest.java    |   10 +-
 .../plugin/consul/ConsulCoordinatorTest.java       |    4 +-
 .../cluster-etcd-plugin/pom.xml                    |    6 +
 .../cluster/plugin/etcd/ClusterEtcdPluginIT.java   |    6 +-
 .../ClusterModuleEtcdProviderFunctionalIT.java     |    6 +-
 .../ClusterModuleKubernetesProviderTest.java       |    6 +-
 .../cluster-nacos-plugin/pom.xml                   |    6 +
 .../ClusterModuleNacosProviderFunctionalIT.java    |    6 +-
 .../cluster/plugin/nacos/NacosCoordinatorTest.java |    4 +-
 .../cluster-zookeeper-plugin/pom.xml               |    6 +
 ...ClusterModuleZookeeperProviderFunctionalIT.java |    6 +-
 .../plugin/zookeeper/ZookeeperCoordinatorTest.java |    4 +-
 oap-server/server-core/pom.xml                     |    9 +-
 .../core/config/HierarchyDefinitionService.java    |  136 +-
 .../server/core/hierarchy/HierarchyService.java    |   37 +-
 .../openapi/EndpointGroupingBenchmark4Openapi.java |  110 ++
 .../group/uri/RegexVSQuickMatchBenchmark.java      |  135 ++
 .../analyze/EBPFProfilingAnalyzerBenchmark.java    |  199 +++
 .../cilium-fetcher-plugin/pom.xml                  |    6 +
 .../cilium/nodes/CiliumNodeManagerTest.java        |    6 +-
 .../agent/kafka/provider/KafkaFetcherProvider.java |    2 +-
 .../agent/kafka/provider/handler/LogHandler.java   |    4 +-
 .../kafka/provider/handler/LogHandlerTest.java     |    4 +-
 .../oap/server/library/batchqueue/BatchQueue.java  |   10 +-
 .../library/batchqueue/BatchQueueBenchmark.java    |    2 +-
 .../library/batchqueue/RebalanceBenchmark.java     |    8 +-
 .../oap/server/library/server/grpc/GRPCServer.java |   12 +-
 .../oap/server/library/server/http/HTTPServer.java |   10 +-
 oap-server/server-library/library-util/pom.xml     |    5 +
 .../library/util/StringFormatGroupBenchmark.java   |   74 +
 .../oap/query/graphql/resolver/LogTestQuery.java   |   25 +-
 .../oap/query/graphql/resolver/PprofQuery.java     |    2 +-
 .../query/graphql/resolver/LogTestQueryTest.java   |    6 +-
 .../pom.xml                                        |    6 +
 .../discovery/AgentConfigurationsWatcherTest.java  |   10 +-
 .../envoy-metrics-receiver-plugin/pom.xml          |    6 +
 .../envoy/EnvoyHTTPLALSourceTypeProvider.java}     |   32 +-
 .../receiver/envoy/EnvoyMetricReceiverConfig.java  |    4 +-
 .../receiver/envoy/MetricServiceGRPCHandler.java   |    6 +-
 .../envoy/persistence/LogsPersistence.java         |    4 +-
 .../envoy/persistence/TCPLogsPersistence.java      |    4 +-
 ....oap.log.analyzer.v2.spi.LALSourceTypeProvider} |    2 +-
 .../envoy/ClusterManagerMetricsAdapterTest.java    |    4 +-
 .../otel/otlp/OpenTelemetryLogHandler.java         |    4 +-
 .../otlp/OpenTelemetryMetricRequestProcessor.java  |   10 +-
 .../provider/handler/AccessLogServiceHandler.java  |    2 +-
 .../receiver/log/provider/LogModuleProvider.java   |    2 +-
 .../handler/grpc/LogReportServiceGrpcHandler.java  |    4 +-
 .../handler/rest/LogReportServiceHTTPHandler.java  |    4 +-
 .../skywalking-telegraf-receiver-plugin/pom.xml    |    6 +
 .../provider/TelegrafReceiverProvider.java         |    4 +-
 .../provider/handler/TelegrafServiceHandler.java   |   10 +-
 .../receiver/telegraf/TelegrafMetricsTest.java     |   14 +-
 .../skywalking-zabbix-receiver-plugin/pom.xml      |    6 +
 .../receiver/zabbix/provider/ZabbixMetrics.java    |    8 +-
 .../zabbix/provider/config/ZabbixConfig.java       |    3 +-
 .../zabbix/provider/ZabbixMetricsTest.java         |    8 +-
 oap-server/server-starter/pom.xml                  |    7 +
 .../src/main/resources/hierarchy-definition.yml    |    7 +-
 .../src/main/resources/lal/envoy-als.yaml          |    5 +
 .../storage-banyandb-plugin/pom.xml                |    6 +
 .../server/storage/plugin/banyandb/BanyanDBIT.java |    4 +-
 .../oap/server/testing/util/ReflectUtil.java       |  157 ++
 .../tool-profile-snapshot-bootstrap/pom.xml        |    6 +
 .../exporter/test/ProfileSnapshotExporterTest.java |    6 +-
 pom.xml                                            |   19 +-
 .../hierarchy-v1-v2-checker}/pom.xml               |   76 +-
 .../oap/server/core/config/HierarchyBenchmark.java |  233 +++
 .../core/config/HierarchyRuleComparisonTest.java   |  191 +++
 .../src/test}/resources/hierarchy-definition.yml   |    0
 .../hierarchy-v1-with-groovy}/pom.xml              |   14 +-
 .../core/config/GroovyHierarchyRuleProvider.java   |   49 +
 .../lal-v1-with-groovy}/pom.xml                    |   19 +-
 .../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/spec/AbstractSpec.java    |    0
 .../log/analyzer/dsl/spec/LALDelegatingScript.java |    0
 .../analyzer/dsl/spec/extractor/ExtractorSpec.java |   88 ++
 .../extractor/sampledtrace/SampledTraceSpec.java   |    0
 .../dsl/spec/extractor/slowsql/SlowSqlSpec.java    |    0
 .../log/analyzer/dsl/spec/filter/FilterSpec.java   |  166 +++
 .../dsl/spec/parser/AbstractParserSpec.java        |    0
 .../analyzer/dsl/spec/parser/JsonParserSpec.java   |    0
 .../analyzer/dsl/spec/parser/TextParserSpec.java   |    0
 .../analyzer/dsl/spec/parser/YamlParserSpec.java   |    0
 .../log/analyzer/dsl/spec/sink/SamplerSpec.java    |   32 +
 .../oap/log/analyzer/dsl/spec/sink/SinkSpec.java   |   22 +
 .../dsl/spec/sink/sampler/PossibilitySampler.java  |    0
 .../dsl/spec/sink/sampler/RateLimitingSampler.java |    0
 .../analyzer/dsl/spec/sink/sampler/Sampler.java    |    0
 .../oap/log/analyzer/module/LogAnalyzerModule.java |    0
 .../oap/log/analyzer/provider/LALConfig.java       |    0
 .../oap/log/analyzer/provider/LALConfigs.java      |    0
 .../analyzer/provider/LogAnalyzerModuleConfig.java |    0
 .../provider/LogAnalyzerModuleProvider.java        |    0
 .../provider/log/ILogAnalysisListenerManager.java  |    0
 .../analyzer/provider/log/ILogAnalyzerService.java |    0
 .../oap/log/analyzer/provider/log/LogAnalyzer.java |    0
 .../provider/log/LogAnalyzerServiceImpl.java       |    0
 .../provider/log/analyzer/LogAnalyzerFactory.java  |    0
 .../provider/log/listener/LogAnalysisListener.java |    0
 .../log/listener/LogAnalysisListenerFactory.java   |    0
 .../provider/log/listener/LogFilterListener.java   |    0
 .../provider/log/listener/LogSinkListener.java     |    0
 .../log/listener/LogSinkListenerFactory.java       |    0
 .../provider/log/listener/RecordSinkListener.java  |    0
 .../provider/log/listener/TrafficSinkListener.java |    0
 .../analyzer/dsl/registry/ProcessRegistry.java     |   53 +-
 ...ywalking.oap.server.library.module.ModuleDefine |    0
 ...alking.oap.server.library.module.ModuleProvider |    0
 .../oap/log/analyzer/dsl/DSLSecurityTest.java      |    9 +-
 .../skywalking/oap/log/analyzer/dsl/DSLTest.java   |    9 +-
 .../test/resources/log-mal-rules/placeholder.yaml  |    0
 .../org.mockito.plugins.MockMaker                  |    0
 .../mal-lal-v1-v2-checker/CLAUDE.md                |  106 ++
 .../mal-lal-v1-v2-checker/pom.xml                  |  120 ++
 .../analyzer/dsl/registry/ProcessRegistry.java     |   28 +-
 .../analyzer/v2/dsl/registry/ProcessRegistry.java  |   28 +-
 .../oap/server/checker/InMemoryCompiler.java       |  118 ++
 .../oap/server/checker/lal/LalBenchmark.java       |  501 +++++++
 .../oap/server/checker/lal/LalComparisonTest.java  |  792 ++++++++++
 .../checker/lal/TestMeshLALSourceTypeProvider.java |   30 +-
 .../oap/server/checker/mal/MalBenchmark.java       |  342 +++++
 .../oap/server/checker/mal/MalComparisonTest.java  | 1107 ++++++++++++++
 .../checker/mal/MalExpectedDataGenerator.java      |  597 ++++++++
 .../checker/mal/MalExpectedDataGeneratorTest.java  |   49 +
 .../checker/mal/MalFilterComparisonTest.java       |  205 +++
 .../server/checker/mal/MalInputDataGenerator.java  |  935 ++++++++++++
 .../checker/mal/MalInputDataGeneratorTest.java     |   65 +
 ...g.oap.log.analyzer.v2.spi.LALSourceTypeProvider |    2 +-
 .../mal-v1-with-groovy}/pom.xml                    |   17 +-
 .../skywalking/oap/meter/analyzer/Analyzer.java    |    0
 .../oap/meter/analyzer/MetricConvert.java          |    0
 .../oap/meter/analyzer/MetricRuleConfig.java       |    0
 .../skywalking/oap/meter/analyzer/dsl/DSL.java     |    1 +
 .../oap/meter/analyzer/dsl/DownsamplingType.java   |    0
 .../EndpointEntityDescription.java                 |    0
 .../dsl/EntityDescription/EntityDescription.java   |    0
 .../InstanceEntityDescription.java                 |    4 +-
 .../ProcessEntityDescription.java                  |    0
 .../ProcessRelationEntityDescription.java          |    0
 .../ServiceEntityDescription.java                  |    0
 .../ServiceRelationEntityDescription.java          |    0
 .../oap/meter/analyzer/dsl/Expression.java         |    0
 .../analyzer/dsl/ExpressionParsingContext.java     |    4 +-
 .../analyzer/dsl/ExpressionParsingException.java   |    0
 .../oap/meter/analyzer/dsl/FilterExpression.java   |    0
 .../oap/meter/analyzer/dsl/NumberClosure.java      |    0
 .../skywalking/oap/meter/analyzer/dsl/Result.java  |    0
 .../skywalking/oap/meter/analyzer/dsl/Sample.java  |    0
 .../oap/meter/analyzer/dsl/SampleFamily.java       |   93 +-
 .../meter/analyzer/dsl/SampleFamilyBuilder.java    |    0
 .../meter/analyzer/dsl/SampleFamilyFunctions.java  |   77 +
 .../meter/analyzer/dsl/counter/CounterWindow.java  |    0
 .../oap/meter/analyzer/dsl/counter/ID.java         |    0
 .../analyzer/dsl/registry/ProcessRegistry.java     |    0
 .../meter/analyzer/dsl/tagOpt/K8sRetagType.java    |    0
 .../oap/meter/analyzer/dsl/tagOpt/Retag.java       |    0
 .../oap/meter/analyzer/k8s/K8sInfoRegistry.java    |    0
 .../prometheus/PrometheusMetricConverter.java      |    0
 .../analyzer/prometheus/rule/MetricsRule.java      |    0
 .../oap/meter/analyzer/prometheus/rule/Rule.java   |    0
 .../oap/meter/analyzer/prometheus/rule/Rules.java  |    0
 .../oap/meter/analyzer/MetricConvertTest.java      |    0
 .../oap/meter/analyzer/dsl/AggregationTest.java    |    0
 .../oap/meter/analyzer/dsl/AnalyzerTest.java       |    4 +-
 .../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         |    6 +-
 .../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
 .../script-runtime-with-groovy}/pom.xml            |   23 +-
 .../test-hierarchy-definition.data.yaml            |  111 ++
 .../hierarchy-rule/test-hierarchy-definition.yml   |    0
 .../feature-cases/execution-basic.input.data       |  161 ++
 .../test-lal/feature-cases/execution-basic.yaml    |  272 ++++
 .../lal/test-lal/oap-cases/default.input.data      |    9 +-
 .../scripts/lal/test-lal/oap-cases/default.yaml    |   13 +-
 .../lal/test-lal/oap-cases/envoy-als.input.data    |  111 ++
 .../scripts/lal/test-lal/oap-cases}/envoy-als.yaml |    2 +
 .../lal/test-lal/oap-cases/k8s-service.input.data  |  123 ++
 .../lal/test-lal/oap-cases/k8s-service.yaml        |   37 +-
 .../lal/test-lal/oap-cases/mesh-dp.input.data      |   89 ++
 .../scripts/lal/test-lal/oap-cases/mesh-dp.yaml    |   36 +-
 .../test-lal/oap-cases/mysql-slowsql.input.data    |   15 +-
 .../lal/test-lal/oap-cases/mysql-slowsql.yaml      |   21 +-
 .../oap-cases/network-profiling-e2e.input.data     |   46 +
 .../test-lal/oap-cases/network-profiling-e2e.yaml  |   57 +-
 .../lal/test-lal/oap-cases/nginx.input.data        |   21 +-
 .../scripts/lal/test-lal/oap-cases/nginx.yaml      |   60 +
 .../test-lal/oap-cases/pgsql-slowsql.input.data    |   15 +-
 .../lal/test-lal/oap-cases/pgsql-slowsql.yaml      |   21 +-
 .../test-lal/oap-cases/redis-slowsql.input.data    |   15 +-
 .../lal/test-lal/oap-cases/redis-slowsql.yaml      |   21 +-
 .../test-envoy-metrics-rules/envoy-ca.data.yaml    |   75 +
 .../mal/test-envoy-metrics-rules/envoy-ca.yaml     |   60 +
 .../envoy-svc-relation.data.yaml                   |  116 ++
 .../envoy-svc-relation.yaml                        |   48 +
 .../mal/test-envoy-metrics-rules/envoy.data.yaml   |  312 ++++
 .../mal/test-envoy-metrics-rules/envoy.yaml        |   77 +
 .../scripts/mal/test-log-mal-rules/nginx.data.yaml |   32 +-
 .../scripts/mal/test-log-mal-rules/nginx.yaml      |   14 +-
 .../continuous-profiling.data.yaml                 |  121 ++
 .../continuous-profiling.yaml                      |   14 +-
 .../datasource.data.yaml                           |   23 +-
 .../mal/test-meter-analyzer-config/datasource.yaml |    9 +-
 .../test-meter-analyzer-config/go-agent.data.yaml  |  179 +++
 .../mal/test-meter-analyzer-config/go-agent.yaml   |   32 +
 .../go-runtime.data.yaml                           |  359 +++++
 .../mal/test-meter-analyzer-config/go-runtime.yaml |   80 +
 .../java-agent.data.yaml                           |  181 +++
 .../mal/test-meter-analyzer-config/java-agent.yaml |   32 +
 .../network-profiling-ebpf.data.yaml               |  113 ++
 .../network-profiling-ebpf.yaml                    |   60 +
 .../network-profiling.data.yaml                    | 1110 ++++++++++++++
 .../network-profiling.yaml                         |  135 ++
 .../python-runtime.data.yaml                       |  134 ++
 .../test-meter-analyzer-config/python-runtime.yaml |   36 +
 .../ruby-runtime.data.yaml                         |  186 +++
 .../test-meter-analyzer-config/ruby-runtime.yaml   |   53 +
 .../satellite-tag-prefix.data.yaml                 |  141 ++
 .../satellite-tag-prefix.yaml                      |   36 +
 .../test-meter-analyzer-config/satellite.data.yaml |  135 ++
 .../mal/test-meter-analyzer-config/satellite.yaml  |   34 +
 .../spring-micrometer.data.yaml                    |  316 ++++
 .../spring-micrometer.yaml                         |   64 +
 .../threadpool.data.yaml                           |   23 +-
 .../mal/test-meter-analyzer-config/threadpool.yaml |    9 +-
 .../activemq/activemq-broker.data.yaml             |  383 +++++
 .../test-otel-rules/activemq/activemq-broker.yaml  |   97 ++
 .../activemq/activemq-cluster.data.yaml            |  311 ++++
 .../test-otel-rules/activemq/activemq-cluster.yaml |   95 ++
 .../activemq/activemq-destination.data.yaml        |  280 ++++
 .../activemq/activemq-destination.yaml             |   79 +
 .../scripts/mal/test-otel-rules/apisix.data.yaml   |  549 +++++++
 .../scripts/mal/test-otel-rules/apisix.yaml        |  102 ++
 .../aws-dynamodb/dynamodb-endpoint.data.yaml       |  359 +++++
 .../aws-dynamodb/dynamodb-endpoint.yaml            |   85 ++
 .../aws-dynamodb/dynamodb-service.data.yaml        |  427 ++++++
 .../aws-dynamodb/dynamodb-service.yaml             |  108 ++
 .../test-otel-rules/aws-eks/eks-cluster.data.yaml  |  133 ++
 .../mal/test-otel-rules/aws-eks/eks-cluster.yaml   |   30 +-
 .../mal/test-otel-rules/aws-eks/eks-node.data.yaml |  294 ++++
 .../mal/test-otel-rules/aws-eks/eks-node.yaml      |   69 +
 .../test-otel-rules/aws-eks/eks-service.data.yaml  |  124 ++
 .../mal/test-otel-rules/aws-eks/eks-service.yaml   |   26 +-
 .../aws-gateway/gateway-endpoint.data.yaml         |  293 ++++
 .../aws-gateway/gateway-endpoint.yaml              |   63 +
 .../aws-gateway/gateway-service.data.yaml          |  235 +++
 .../aws-gateway/gateway-service.yaml               |   63 +
 .../test-otel-rules/aws-s3/s3-service.data.yaml    |  177 +++
 .../mal/test-otel-rules/aws-s3/s3-service.yaml     |   33 +-
 .../banyandb/banyandb-instance.data.yaml           |  479 ++++++
 .../banyandb/banyandb-instance.yaml                |   86 ++
 .../banyandb/banyandb-service.data.yaml            |  453 ++++++
 .../test-otel-rules/banyandb/banyandb-service.yaml |   86 ++
 .../bookkeeper/bookkeeper-cluster.data.yaml        |  167 +++
 .../bookkeeper/bookkeeper-cluster.yaml             |   40 +-
 .../bookkeeper/bookkeeper-node.data.yaml           |  401 +++++
 .../bookkeeper/bookkeeper-node.yaml                |   90 ++
 .../clickhouse/clickhouse-instance.data.yaml       |  732 ++++++++++
 .../clickhouse/clickhouse-instance.yaml            |  178 +++
 .../clickhouse/clickhouse-service.data.yaml        |  607 ++++++++
 .../clickhouse/clickhouse-service.yaml             |  162 +++
 .../elasticsearch/elasticsearch-cluster.data.yaml  |  205 +++
 .../elasticsearch/elasticsearch-cluster.yaml       |   72 +
 .../elasticsearch/elasticsearch-index.data.yaml    |  703 +++++++++
 .../elasticsearch/elasticsearch-index.yaml         |  127 ++
 .../elasticsearch/elasticsearch-node.data.yaml     |  820 +++++++++++
 .../elasticsearch/elasticsearch-node.yaml          |  149 ++
 .../mal/test-otel-rules/flink/flink-job.data.yaml  |  267 ++++
 .../mal/test-otel-rules/flink/flink-job.yaml       |   72 +
 .../flink/flink-jobManager.data.yaml               |  294 ++++
 .../test-otel-rules/flink/flink-jobManager.yaml    |   80 +
 .../flink/flink-taskManager.data.yaml              |  413 ++++++
 .../test-otel-rules/flink/flink-taskManager.yaml   |   89 ++
 .../test-otel-rules/istio-controlplane.data.yaml   |  514 +++++++
 .../mal/test-otel-rules/istio-controlplane.yaml    |  108 ++
 .../mal/test-otel-rules/k8s/k8s-cluster.data.yaml  |  401 +++++
 .../mal/test-otel-rules/k8s/k8s-cluster.yaml       |   94 ++
 .../mal/test-otel-rules/k8s/k8s-instance.data.yaml |   26 +-
 .../mal/test-otel-rules/k8s/k8s-instance.yaml      |   12 +-
 .../mal/test-otel-rules/k8s/k8s-node.data.yaml     |  284 ++++
 .../scripts/mal/test-otel-rules/k8s/k8s-node.yaml  |   74 +
 .../mal/test-otel-rules/k8s/k8s-service.data.yaml  |  208 +++
 .../mal/test-otel-rules/k8s/k8s-service.yaml       |   59 +
 .../test-otel-rules/kafka/kafka-broker.data.yaml   |  465 ++++++
 .../mal/test-otel-rules/kafka/kafka-broker.yaml    |  118 ++
 .../test-otel-rules/kafka/kafka-cluster.data.yaml  |  137 ++
 .../mal/test-otel-rules/kafka/kafka-cluster.yaml   |   36 +-
 .../test-otel-rules/kong/kong-endpoint.data.yaml   |  227 +++
 .../mal/test-otel-rules/kong/kong-endpoint.yaml    |   40 +
 .../test-otel-rules/kong/kong-instance.data.yaml   |  351 +++++
 .../mal/test-otel-rules/kong/kong-instance.yaml    |  105 ++
 .../test-otel-rules/kong/kong-service.data.yaml    |  303 ++++
 .../mal/test-otel-rules/kong/kong-service.yaml     |   69 +
 .../mongodb/mongodb-cluster.data.yaml              |  248 ++++
 .../test-otel-rules/mongodb/mongodb-cluster.yaml   |   63 +
 .../test-otel-rules/mongodb/mongodb-node.data.yaml |  547 +++++++
 .../mal/test-otel-rules/mongodb/mongodb-node.yaml  |  108 ++
 .../test-otel-rules/mysql/mysql-instance.data.yaml |  325 +++++
 .../mal/test-otel-rules/mysql/mysql-instance.yaml  |   82 ++
 .../test-otel-rules/mysql/mysql-service.data.yaml  |  254 ++++
 .../mal/test-otel-rules/mysql/mysql-service.yaml   |   74 +
 .../test-otel-rules/nginx/nginx-endpoint.data.yaml |  158 ++
 .../mal/test-otel-rules/nginx/nginx-endpoint.yaml  |   27 +-
 .../test-otel-rules/nginx/nginx-instance.data.yaml |  175 +++
 .../mal/test-otel-rules/nginx/nginx-instance.yaml  |   29 +-
 .../test-otel-rules/nginx/nginx-service.data.yaml  |  162 +++
 .../mal/test-otel-rules/nginx/nginx-service.yaml   |   29 +-
 .../scripts/mal/test-otel-rules/oap.data.yaml      | 1539 ++++++++++++++++++++
 .../scripts/mal/test-otel-rules/oap.yaml           |  144 ++
 .../postgresql/postgresql-instance.data.yaml       |  597 ++++++++
 .../postgresql/postgresql-instance.yaml            |  115 ++
 .../postgresql/postgresql-service.data.yaml        |  368 +++++
 .../postgresql/postgresql-service.yaml             |   95 ++
 .../test-otel-rules/pulsar/pulsar-broker.data.yaml |  281 ++++
 .../mal/test-otel-rules/pulsar/pulsar-broker.yaml  |   76 +
 .../pulsar/pulsar-cluster.data.yaml                |  197 +++
 .../mal/test-otel-rules/pulsar/pulsar-cluster.yaml |   46 +-
 .../rabbitmq/rabbitmq-cluster.data.yaml            |  362 +++++
 .../test-otel-rules/rabbitmq/rabbitmq-cluster.yaml |   86 ++
 .../rabbitmq/rabbitmq-node.data.yaml               |  377 +++++
 .../test-otel-rules/rabbitmq/rabbitmq-node.yaml    |   80 +
 .../test-otel-rules/redis/redis-instance.data.yaml |  212 +++
 .../mal/test-otel-rules/redis/redis-instance.yaml  |   69 +
 .../test-otel-rules/redis/redis-service.data.yaml  |  243 ++++
 .../mal/test-otel-rules/redis/redis-service.yaml   |   70 +
 .../rocketmq/rocketmq-broker.data.yaml             |   81 ++
 .../test-otel-rules/rocketmq/rocketmq-broker.yaml  |   24 +-
 .../rocketmq/rocketmq-cluster.data.yaml            |  219 +++
 .../test-otel-rules/rocketmq/rocketmq-cluster.yaml |   81 ++
 .../rocketmq/rocketmq-topic.data.yaml              |  181 +++
 .../test-otel-rules/rocketmq/rocketmq-topic.yaml   |   48 +-
 .../service-decorate-attributes.data.yaml          |   24 +-
 .../service-decorate-attributes.yaml               |   40 +
 .../service-gstring-regex-split.data.yaml          |   24 +-
 .../service-gstring-regex-split.yaml               |   35 +
 .../scripts/mal/test-otel-rules/vm.data.yaml       |  270 ++++
 .../scripts/mal/test-otel-rules/vm.yaml            |   97 ++
 .../scripts/mal/test-otel-rules/windows.data.yaml  |  147 ++
 .../scripts/mal/test-otel-rules/windows.yaml       |   72 +
 .../scripts/mal/test-telegraf-rules/vm.data.yaml   |  278 ++++
 .../scripts/mal/test-telegraf-rules/vm.yaml        |   72 +
 .../scripts/mal/test-zabbix-rules/agent.data.yaml  |  211 +++
 .../scripts/mal/test-zabbix-rules/agent.yaml       |   89 ++
 534 files changed, 53306 insertions(+), 2835 deletions(-)

diff --git a/.claude/skills/ci-e2e-debug/SKILL.md b/.claude/skills/ci-e2e-debug/SKILL.md
new file mode 100644
index 0000000000..23723d85e9
--- /dev/null
+++ b/.claude/skills/ci-e2e-debug/SKILL.md
@@ -0,0 +1,41 @@
+---
+name: ci-e2e-debug
+description: Download and inspect CI e2e test logs from GitHub Actions artifacts. Use when investigating e2e test failures in CI.
+argument-hint: "<RUN_ID or GitHub Actions URL>"
+---
+
+# CI E2E Debug Skill
+
+Download test log artifacts from a GitHub Actions CI run and inspect them for errors.
+
+## Steps
+
+1. **Find the artifact**: Use `gh api` to list artifacts for the given CI run:
+   ```bash
+   gh api repos/apache/skywalking/actions/runs/<RUN_ID>/artifacts --jq '.artifacts[] | {id: .id, name: .name}'
+   ```
+
+2. **Download and extract**: Download the artifact zip and extract it:
+   ```bash
+   cd /tmp && rm -rf e2e-debug-logs && mkdir e2e-debug-logs && cd e2e-debug-logs
+   gh api repos/apache/skywalking/actions/artifacts/<ARTIFACT_ID>/zip > artifact.zip
+   unzip -o artifact.zip
+   ```
+
+3. **Inspect OAP logs**: Look for errors in the OAP server logs:
+   ```bash
+   # Find OAP log files
+   find /tmp/e2e-debug-logs -name "skywalking-oap-*.log" -o -name "oap.log"
+   # Check for errors
+   grep -E "ERROR|Exception|FATAL|CannotCompileException" <log_file> | head -30
+   ```
+
+4. **Inspect other component logs**: Check BanyanDB, UI, and other pod logs as needed.
+
+5. **Report findings**: Summarize the root cause error from the logs.
+
+## Notes
+- CI artifacts are automatically uploaded by the e2e test framework to `$SW_INFRA_E2E_LOG_DIR`
+- Log files are organized by namespace/pod name
+- OAP init pods may have different errors than the main OAP pod — check all of them
+- Common errors: MAL/LAL/OAL compilation failures, storage connection issues, module initialization errors
diff --git a/.claude/skills/compile/SKILL.md b/.claude/skills/compile/SKILL.md
new file mode 100644
index 0000000000..b13537057a
--- /dev/null
+++ b/.claude/skills/compile/SKILL.md
@@ -0,0 +1,109 @@
+---
+name: compile
+description: Build SkyWalking OAP server, run javadoc checks, and verify checkstyle. Use to validate changes before submitting a PR.
+argument-hint: "[all|backend|javadoc|checkstyle|module-name]"
+---
+
+# Compile & Verify
+
+Build the project and run static checks matching CI.
+
+## Prerequisites
+
+- JDK 11, 17, or 21 (LTS versions)
+- Maven 3.6+ (use `./mvnw` wrapper)
+
+## Maven profiles
+
+- `backend` (default): Builds OAP server modules
+- `ui` (default): Builds web application
+- `dist` (default): Creates distribution packages
+- `all`: Builds everything including submodule initialization
+
+## Commands by argument
+
+### `all` or no argument — full CI build
+
+```bash
+./mvnw clean flatten:flatten install javadoc:javadoc -B -q -Pall \
+  -Dmaven.test.skip \
+  -Dcheckstyle.skip \
+  -Dgpg.skip
+```
+
+### `backend` — backend only (faster)
+
+```bash
+./mvnw clean flatten:flatten package -Pbackend,dist -Dmaven.test.skip
+```
+
+### `javadoc` — javadoc check only
+
+Javadoc requires delombok output, so `install` must run first:
+
+```bash
+./mvnw clean flatten:flatten install javadoc:javadoc -B -q -Pall \
+  -Dmaven.test.skip \
+  -Dcheckstyle.skip \
+  -Dgpg.skip
+```
+
+Running `javadoc:javadoc` alone without `install` will miss errors because `${delombok.output.dir}` won't be populated.
+
+### `checkstyle` — checkstyle only
+
+```bash
+./mvnw -B -q clean flatten:flatten checkstyle:check
+```
+
+### Module name — single module build
+
+```bash
+./mvnw clean flatten:flatten package -pl oap-server/analyzer/<module-name> -Dmaven.test.skip
+```
+
+## Reading javadoc output
+
+Maven prefixes all javadoc output with `[ERROR]`, but the actual severity is in the message after the line number. Only lines containing `error:` fail the build; lines with `warning:` do not.
+
+```
+[ERROR] Foo.java:42: error: bad use of '>'        ← ACTUAL ERROR (must fix)
+[ERROR] Foo.java:50: warning: no @param for <T>   ← WARNING (does not fail build)
+```
+
+### Common javadoc errors
+
+| Error | Cause | Fix |
+|-------|-------|-----|
+| `bad use of '>'` | Bare `>` in javadoc HTML (e.g., `->` in `<pre>` blocks) | Use `{@code ->}` or `-&gt;` |
+| heading out of sequence | Heading level skips the expected hierarchy | See heading rules below |
+| reference not found | `{@link Foo#bar()}` with wrong signature | Match exact parameter types: `{@link Foo#bar(ArgType)}` |
+
+### Javadoc heading rules (JDK 13+)
+
+Strict heading validation was introduced in JDK 13. JDK 11 does **not** enforce it, but JDK 17/21/25 do. Write headings correctly for forward compatibility:
+
+| Javadoc location | Start heading at |
+|---|---|
+| Class, interface, enum, package, module | `<h2>` |
+| Constructor, method, field | `<h4>` |
+| Standalone HTML files (`doc-files/`) | `<h1>` |
+
+The generated javadoc page uses `<h1>` for the class name and `<h3>` for member sections (Methods, Fields, etc.), so class-level subsections must use `<h2>` and method-level subsections must use `<h4>` to maintain proper nesting.
+
+## CI reference
+
+CI uses JDK 11 on Linux. The `dist-tar` job runs:
+
+```bash
+./mvnw clean flatten:flatten install javadoc:javadoc -B -q -Pall \
+  -Dmaven.test.skip \
+  -Dcheckstyle.skip \
+  -Dgpg.skip
+```
+
+The `code-style` job runs:
+
+```bash
+./mvnw -B -q clean flatten:flatten checkstyle:check
+```
diff --git a/.claude/skills/generate-classes/SKILL.md b/.claude/skills/generate-classes/SKILL.md
new file mode 100644
index 0000000000..578ed50418
--- /dev/null
+++ b/.claude/skills/generate-classes/SKILL.md
@@ -0,0 +1,65 @@
+---
+name: generate-classes
+description: Generate bytecode classes from DSL scripts (MAL, OAL, LAL, Hierarchy). Runs the compiler and dumps .class files for inspection.
+argument-hint: "<mal|oal|lal|hierarchy|all>"
+---
+
+# Generate DSL Classes
+
+Run the v2 compiler (ANTLR4 + Javassist) to generate bytecode classes from DSL scripts and dump `.class` files to disk for inspection.
+
+## Commands by argument
+
+### `mal` — MAL expression classes
+
+```bash
+./mvnw test -pl test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker \
+  -Dtest=MalComparisonTest -DfailIfNoTests=false -Dcheckstyle.skip
+```
+
+Output location: `test/script-cases/scripts/mal/**/*.generated-classes/`
+
+### `oal` — OAL metrics/dispatcher/builder classes
+
+```bash
+./mvnw test -pl oap-server/oal-rt \
+  -Dtest=RuntimeOALGenerationTest -DfailIfNoTests=false -Dcheckstyle.skip
+```
+
+Output location: `oap-server/oal-rt/target/test-classes/metrics/`, `metrics/builder/`, `dispatcher/`
+
+### `lal` — LAL filter/extractor classes
+
+```bash
+./mvnw test -pl test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker \
+  -Dtest=LalComparisonTest -DfailIfNoTests=false -Dcheckstyle.skip
+```
+
+Output location: `test/script-cases/scripts/lal/**/*.generated-classes/`
+
+### `hierarchy` — Hierarchy rule classes
+
+```bash
+./mvnw test -pl test/script-cases/script-runtime-with-groovy/hierarchy-v1-v2-checker \
+  -Dtest=HierarchyRuleComparisonTest -DfailIfNoTests=false -Dcheckstyle.skip
+```
+
+Output location: `test/script-cases/scripts/hierarchy-rule/*.generated-classes/`
+
+### `all` or no argument — generate all DSLs
+
+Run all four commands above sequentially.
+
+## After generation
+
+Print the output location for the requested DSL so the user knows where to find the generated `.class` files. Use `javap` to decompile:
+
+```bash
+javap -c -p <path-to-class-file>
+```
+
+## Cleaning generated classes
+
+```bash
+./mvnw clean -pl test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker,test/script-cases/script-runtime-with-groovy/hierarchy-v1-v2-checker
+```
diff --git a/.claude/skills/gh-pull-request/SKILL.md b/.claude/skills/gh-pull-request/SKILL.md
new file mode 100644
index 0000000000..fa08cd598f
--- /dev/null
+++ b/.claude/skills/gh-pull-request/SKILL.md
@@ -0,0 +1,112 @@
+---
+name: gh-pull-request
+description: Verify, commit, and push changes on a PR branch. Runs pre-flight checks (compile, checkstyle, license headers) before every push. Also creates the PR if one doesn't exist yet.
+---
+
+# PR Branch Workflow
+
+Run pre-flight checks, commit, push, and optionally create a PR.
+
+## Pre-flight checks
+
+Run these checks before every commit+push and fix any failures:
+
+### 1. Compile and checkstyle
+
+```bash
+# Checkstyle
+./mvnw -B -q clean checkstyle:check
+
+# Full build (compile + javadoc)
+./mvnw clean flatten:flatten install javadoc:javadoc -B -q -Pall \
+  -Dmaven.test.skip \
+  -Dcheckstyle.skip \
+  -Dgpg.skip
+```
+
+### 2. License header check
+
+```bash
+license-eye header check
+```
+
+If invalid files are found, fix with `license-eye header fix` and re-check.
+
+## Commit and push
+
+After checks pass, commit and push:
+
+```bash
+git add <files>
+git commit -m "<message>"
+git push -u origin <branch-name>
+```
+
+### Branch strategy
+- **Never work directly on master branch**
+- If on master, create a new branch first: `git checkout -b feature/<name>` or `git checkout -b fix/<name>`
+
+## Create PR (if not yet created)
+
+Check whether a PR already exists for the current branch:
+
+```bash
+gh pr view --json number 2>/dev/null
+```
+
+If no PR exists, create one:
+
+### PR title
+Summarize the changes concisely. Examples:
+- `Fix BanyanDB query timeout issue`
+- `Add support for OpenTelemetry metrics`
+
+### PR description
+
+Read `.github/PULL_REQUEST_TEMPLATE` and use its **exact format with checkboxes**. Do NOT use a custom summary format.
+
+Key template sections — uncomment the relevant one:
+
+**For Bug Fixes:**
+```
+### Fix <bug description or issue link>
+- [ ] Add a unit test to verify that the fix works.
+- [ ] Explain briefly why the bug exists and how to fix it.
+```
+
+**For New Features:**
+```
+### <Feature description>
+- [ ] If this is non-trivial feature, paste the links/URLs to the design doc.
+- [ ] Update the documentation to include this new feature.
+- [ ] Tests(including UT, IT, E2E) are added to verify the new feature.
+- [ ] If it's UI related, attach the screenshots below.
+```
+
+**For Performance Improvements:**
+```
+### Improve the performance of <class or module or ...>
+- [ ] Add a benchmark for the improvement.
+- [ ] The benchmark result.
+- [ ] Links/URLs to the theory proof or discussion articles/blogs.
+```
+
+**Always include:**
+```
+- [ ] If this pull request closes/resolves/fixes an existing issue, replace the issue number. Closes #<issue number>.
+- [ ] Update the [`CHANGES` log](https://github.com/apache/skywalking/blob/master/docs/en/changes/changes.md).
+```
+
+### Create command
+
+```bash
+gh pr create --title "<title>" --body "$(cat <<'EOF'
+<PR body from template>
+EOF
+)"
+```
+
+### Post-creation
+- Add `copilot` as a reviewer: `gh pr edit <number> --add-reviewer copilot`
+- Do NOT add AI assistant as co-author. Code responsibility is on the committer's hands.
+- Return the PR URL when done.
diff --git a/.claude/skills/license/SKILL.md b/.claude/skills/license/SKILL.md
new file mode 100644
index 0000000000..e1fc9a1913
--- /dev/null
+++ b/.claude/skills/license/SKILL.md
@@ -0,0 +1,62 @@
+---
+name: license
+description: Check and fix Apache 2.0 license headers and dependency licenses using skywalking-eyes. Use before submitting a PR.
+argument-hint: "[check|fix|deps]"
+---
+
+# License Checks
+
+Check and fix license compliance using [skywalking-eyes](https://github.com/apache/skywalking-eyes). Two CI jobs use this tool:
+
+1. **license-header** — verifies all source files have Apache 2.0 headers
+2. **dependency-license** — verifies the LICENSE file matches current dependencies
+
+## Steps
+
+### Header check (default, or `check` argument)
+
+```bash
+license-eye header check
+```
+
+- **All valid**: Output shows `valid: N, invalid: 0` — nothing to do.
+- **Invalid files found**: Fix with `license-eye header fix`, then re-check.
+
+### Header fix (`fix` argument)
+
+```bash
+license-eye header fix
+license-eye header check
+```
+
+### Dependency license check (`deps` argument)
+
+This regenerates the LICENSE file from dependency metadata and checks for drift:
+
+```bash
+license-eye dependency resolve --summary ./dist-material/release-docs/LICENSE.tpl || exit 1
+if [ ! -z "$(git diff -U0 ./dist-material/release-docs/LICENSE)" ]; then
+  echo "LICENSE file is not updated correctly"
+  git diff -U0 ./dist-material/release-docs/LICENSE
+fi
+```
+
+If the LICENSE file changed, review the diff and commit it. **Important**: CI runs on Linux — some dependencies have platform-specific variants. If you're on macOS/Windows, the LICENSE diff may be a platform artifact. Verify before committing.
+
+## Rules
+
+Configuration is in `.licenserc.yaml`:
+- Java, XML, YAML/YML files require Apache 2.0 headers
+- JSON and Markdown files are excluded (JSON doesn't support comments)
+- Generated files and certain vendor paths are excluded
+- SPI service files (`META-INF/services/`) require headers (use `#` comment style)
+
+## Installation
+
+```bash
+# Same version as CI (pinned commit)
+go install github.com/apache/skywalking-eyes/cmd/license-eye@5b7ee1731d036b5aac68f8bd3fc9e6f98ada082e
+
+# Or via Homebrew (macOS)
+brew install license-eye
+```
diff --git a/.claude/skills/run-e2e/SKILL.md b/.claude/skills/run-e2e/SKILL.md
new file mode 100644
index 0000000000..6f6999de85
--- /dev/null
+++ b/.claude/skills/run-e2e/SKILL.md
@@ -0,0 +1,146 @@
+---
+name: run-e2e
+description: Run SkyWalking E2E tests locally
+disable-model-invocation: true
+argument-hint: "[test-case-path]"
+---
+
+# Run SkyWalking E2E Test
+
+Run an E2E test case using `skywalking-infra-e2e`. The user provides a test case path (e.g., `simple/jdk`, `storage/banyandb`, `alarm`).
+
+## Prerequisites
+
+All tools require **Go** installed. Check `.github/workflows/` for the exact `e2e` commit used in CI.
+
+### e2e CLI
+
+Built from [apache/skywalking-infra-e2e](https://github.com/apache/skywalking-infra-e2e), pinned by commit in CI:
+
+```bash
+# Install the pinned commit
+go install github.com/apache/skywalking-infra-e2e/cmd/e2e@<commit-id>
+
+# Or clone and build locally (useful when debugging the e2e tool itself)
+git clone https://github.com/apache/skywalking-infra-e2e.git
+cd skywalking-infra-e2e
+git checkout <commit-id>
+make build
+# binary is in bin/e2e — add to PATH or copy to $GOPATH/bin
+```
+
+### swctl, yq, and other tools
+
+E2E test cases run pre-install steps (see `setup.steps` in each `e2e.yaml`) that install tools into `/tmp/skywalking-infra-e2e/bin`. When running locally, you need these tools on your PATH.
+
+**swctl** — SkyWalking CLI, used in verify cases to query OAP's GraphQL API. Pinned at `SW_CTL_COMMIT` in `test/e2e-v2/script/env`:
+
+```bash
+# Option 1: Use the install script (same as CI)
+bash test/e2e-v2/script/prepare/setup-e2e-shell/install.sh swctl
+export PATH=/tmp/skywalking-infra-e2e/bin:$PATH
+
+# Option 2: Build from source
+go install github.com/apache/skywalking-cli/cmd/swctl@<SW_CTL_COMMIT>
+```
+
+**yq** — YAML processor, used in verify cases:
+
+```bash
+# Option 1: Use the install script
+bash test/e2e-v2/script/prepare/setup-e2e-shell/install.sh yq
+export PATH=/tmp/skywalking-infra-e2e/bin:$PATH
+
+# Option 2: brew install yq (macOS)
+```
+
+**Other tools** (only needed for specific test cases):
+
+| Tool | Install script | Used by |
+|------|---------------|---------|
+| `kubectl` | `install.sh kubectl` | Kubernetes-based tests |
+| `helm` | `install.sh helm` | Helm chart tests |
+| `istioctl` | `install.sh istioctl` | Istio/service mesh tests |
+| `etcdctl` | `install.sh etcdctl` | etcd cluster tests |
+
+All install scripts are at `test/e2e-v2/script/prepare/setup-e2e-shell/`.
+
+## Steps
+
+### 1. Determine the test case
+
+Resolve the user's argument to a full path under `test/e2e-v2/cases/`. If ambiguous, list matching directories and ask.
+
+```bash
+ls test/e2e-v2/cases/<argument>/e2e.yaml
+```
+
+### 2. Check if rebuild is needed
+
+Compare source file timestamps against the last build:
+
+```bash
+# OAP server changes since last build
+find oap-server apm-protocol -type f \( \
+  -name "*.java" -o -name "*.yaml" -o -name "*.yml" -o \
+  -name "*.json" -o -name "*.xml" -o -name "*.properties" -o \
+  -name "*.proto" \
+\) -newer dist/apache-skywalking-apm-bin.tar.gz 2>/dev/null | head -5
+
+# Test service changes since last build
+find test/e2e-v2/java-test-service -type f \( \
+  -name "*.java" -o -name "*.xml" -o -name "*.yaml" -o -name "*.yml" \
+\) -newer test/e2e-v2/java-test-service/e2e-service-provider/target/*.jar 2>/dev/null | head -5
+```
+
+If files are found, warn the user and suggest rebuilding before running.
+
+### 3. Rebuild if needed (only with user confirmation)
+
+```bash
+# Rebuild OAP
+./mvnw clean flatten:flatten package -Pall -Dmaven.test.skip && make docker
+
+# Rebuild test services
+./mvnw -f test/e2e-v2/java-test-service/pom.xml clean flatten:flatten package
+```
+
+### 4. Run the E2E test
+
+Set required environment variables and run:
+
+```bash
+export SW_AGENT_JDK_VERSION=8
+e2e run -c test/e2e-v2/cases/<case-path>/e2e.yaml
+```
+
+### 5. If the test fails
+
+Do NOT run cleanup immediately. Instead:
+
+1. Check container logs:
+   ```bash
+   docker compose -f test/e2e-v2/cases/<case-path>/docker-compose.yml logs oap
+   docker compose -f test/e2e-v2/cases/<case-path>/docker-compose.yml logs provider
+   ```
+
+2. Run verify separately (can retry after investigation):
+   ```bash
+   e2e verify -c test/e2e-v2/cases/<case-path>/e2e.yaml
+   ```
+
+3. Only cleanup when done debugging:
+   ```bash
+   e2e cleanup -c test/e2e-v2/cases/<case-path>/e2e.yaml
+   ```
+
+## Common test cases
+
+| Shorthand | Path |
+|-----------|------|
+| `simple/jdk` | `test/e2e-v2/cases/simple/jdk/` |
+| `storage/banyandb` | `test/e2e-v2/cases/storage/banyandb/` |
+| `storage/elasticsearch` | `test/e2e-v2/cases/storage/elasticsearch/` |
+| `alarm` | `test/e2e-v2/cases/alarm/` |
+| `log` | `test/e2e-v2/cases/log/` |
+| `profiling/trace` | `test/e2e-v2/cases/profiling/trace/` |
diff --git a/.claude/skills/test/SKILL.md b/.claude/skills/test/SKILL.md
new file mode 100644
index 0000000000..cca8166bfc
--- /dev/null
+++ b/.claude/skills/test/SKILL.md
@@ -0,0 +1,81 @@
+---
+name: test
+description: Run unit tests, integration tests, or slow integration tests matching CI. Use to validate changes before submitting a PR.
+argument-hint: "[unit|integration|slow|module-name]"
+---
+
+# Tests
+
+Run tests matching CI configuration.
+
+## Commands by argument
+
+### `unit` or no argument — unit tests
+
+```bash
+./mvnw clean test -q -B -D"checkstyle.skip"
+```
+
+CI runs this on:
+- JDK 11: ubuntu, macOS, Windows
+- JDK 17, 21, 25: ubuntu only
+
+### `integration` — integration tests (excludes slow)
+
+```bash
+./mvnw -B clean integration-test -Dcheckstyle.skip -DskipUTs=true -DexcludedGroups=slow
+```
+
+CI runs this on JDK 11, 17, 21, 25 (ubuntu only).
+
+### `slow` — slow integration tests
+
+```bash
+./mvnw -B clean integration-test -Dcheckstyle.skip -DskipUTs=true \
+  -Dit.test=org.apache.skywalking.library.elasticsearch.ElasticSearchIT \
+  -Dfailsafe.failIfNoSpecifiedTests=false
+```
+
+CI runs on JDK 11 (ubuntu only). Currently only ElasticSearch/OpenSearch IT is in the slow matrix.
+
+### Module name — single module tests
+
+```bash
+# Unit tests for a specific module
+./mvnw clean test -pl oap-server/analyzer/<module-name> -D"checkstyle.skip"
+
+# Integration tests for a specific module
+./mvnw -B clean integration-test -pl oap-server/analyzer/<module-name> -Dcheckstyle.skip -DskipUTs=true
+```
+
+## Test frameworks
+
+- JUnit 5 (`org.junit.jupiter`)
+- Mockito for mocking
+- AssertJ for assertions
+- PowerMock for reflection utilities
+
+## Test naming conventions
+
+| Type | Pattern | Maven phase |
+|------|---------|-------------|
+| Unit tests | `*Test.java` | `test` |
+| Integration tests | `IT*.java` or `*IT.java` | `integration-test` |
+
+## Slow test tagging
+
+Tests tagged with `@Tag("slow")` are excluded from the normal integration-test job and run separately in the slow-integration-test job. Use this tag for tests that spin up external services (Elasticsearch, etc.) and take significant time.
+
+## CI retry behavior
+
+All three CI jobs retry on failure (run the same command twice with `||`). This handles flaky tests but masks intermittent issues — if a test fails locally, investigate rather than relying on retries.
+
+## CI reference
+
+CI workflow: `.github/workflows/skywalking.yaml`
+
+| Job | JDK | OS | Timeout |
+|-----|-----|----|---------|
+| `unit-test` | 11 (3 OS) + 17, 21, 25 (ubuntu) | ubuntu, macOS, Windows | 30 min |
+| `integration-test` | 11, 17, 21, 25 | ubuntu | 60 min |
+| `slow-integration-test` | 11 | ubuntu | 60 min |
diff --git a/.github/workflows/skywalking.yaml b/.github/workflows/skywalking.yaml
index cfa5adb4b8..76abddc09e 100644
--- a/.github/workflows/skywalking.yaml
+++ b/.github/workflows/skywalking.yaml
@@ -82,7 +82,8 @@ jobs:
       - name: Check Dependencies Licenses
         run: |
           go install github.com/apache/skywalking-eyes/cmd/license-eye@5b7ee1731d036b5aac68f8bd3fc9e6f98ada082e
-          license-eye dependency resolve --summary ./dist-material/release-docs/LICENSE.tpl || exit 1
+          ./mvnw flatten:flatten install -Dmaven.test.skip -Dcheckstyle.skip -Dgpg.skip -q
+          license-eye -v debug dependency resolve --summary ./dist-material/release-docs/LICENSE.tpl || exit 1
           if [ ! -z "$(git diff -U0 ./dist-material/release-docs/LICENSE)" ]; then
             echo "LICENSE file is not updated correctly"
             git diff -U0 ./dist-material/release-docs/LICENSE
diff --git a/.gitignore b/.gitignore
index 38241c2e1b..f1e59ed33f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,6 @@
 /build/
 target/
 .idea/
-.flattened-pom.xml
 *.iml
 .classpath
 .project
@@ -33,3 +32,12 @@ oap-server/server-starter/src/main/resources/version.properties
 # Benchmark reports and downloaded tools
 benchmarks/reports/
 benchmarks/.istio/
+
+# Generated .class files from v2 compiler (created during v1-v2 checker tests)
+test/script-cases/scripts/**/*.generated-classes/
+
+# Flattened pom files generated by flatten-maven-plugin
+.flattened-pom.xml
+
+# Claude Code local settings
+.claude/settings.local.json
diff --git a/CLAUDE.md b/CLAUDE.md
index dfa342ad0b..d26fe88a97 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -38,49 +38,6 @@ skywalking/
 └── tools/                         # Development tools
 ```
 
-## Build System
-
-### Prerequisites
-- JDK 11, 17, or 21 (LTS versions)
-- Maven 3.6+
-- Git (with submodule support)
-
-### Common Build Commands
-
-```bash
-# Clone with submodules
-git clone --recurse-submodules https://github.com/apache/skywalking.git
-
-# Or initialize submodules after clone
-git submodule init && git submodule update
-
-# Full build (skip tests)
-./mvnw clean package -Dmaven.test.skip
-
-# Build backend only
-./mvnw package -Pbackend,dist
-# or: make build.backend
-
-# Build UI only
-./mvnw package -Pui,dist
-# or: make build.ui
-
-# Run tests
-./mvnw test
-
-# Run integration tests
-./mvnw integration-test
-
-# Build with all profiles
-./mvnw clean package -Pall -Dmaven.test.skip
-```
-
-### Maven Profiles
-- `backend` (default): Builds OAP server modules
-- `ui` (default): Builds web application
-- `dist` (default): Creates distribution packages
-- `all`: Builds everything including submodule initialization
-
 ## Architecture & Key Concepts
 
 ### Module System
@@ -216,321 +173,6 @@ grep -r "\.toList()" src/ --include="*.java"
 grep -r '"""' src/ --include="*.java"
 ```
 
-## Testing
-
-### Test Frameworks
-- JUnit 5 (`org.junit.jupiter`)
-- Mockito for mocking
-- AssertJ for assertions
-- PowerMock for reflection utilities
-
-### Test Naming
-- Unit tests: `*Test.java`
-- Integration tests: `IT*.java` or `*IT.java`
-
-### Running Tests
-```bash
-# Unit tests only
-./mvnw test
-
-# Integration tests
-./mvnw integration-test
-
-# Skip tests during build
-./mvnw package -Dmaven.test.skip
-```
-
-## E2E Testing
-
-SkyWalking uses [Apache SkyWalking Infra E2E](https://github.com/apache/skywalking-infra-e2e) for end-to-end testing. E2E tests validate the entire system including OAP server, storage backends, agents, and integrations.
-
-### E2E Tool Installation
-
-```bash
-# Install the same version used in CI (recommended)
-go install github.com/apache/skywalking-infra-e2e/cmd/e2e@e7138da4f9b7a25a169c9f8d995795d4d2e34bde
-
-# Verify installation
-e2e --help
-```
-
-### E2E Test Structure
-
-```
-test/e2e-v2/
-├── cases/                    # 50+ test case directories
-│   ├── simple/jdk/           # Basic Java agent test
-│   ├── storage/              # Storage backend tests (BanyanDB, ES, MySQL, PostgreSQL)
-│   ├── alarm/                # Alerting tests
-│   ├── profiling/            # Profiling tests (trace, eBPF, async)
-│   ├── kafka/                # Kafka integration
-│   ├── istio/                # Service mesh tests
-│   └── ...
-├── script/
-│   ├── env                   # Environment variables (agent commits, versions)
-│   ├── docker-compose/
-│   │   └── base-compose.yml  # Base service definitions (oap, banyandb, provider, consumer)
-│   └── prepare/
-│       └── setup-e2e-shell/  # Tool installers (swctl, yq, kubectl, helm)
-└── java-test-service/        # Test service implementations
-    ├── e2e-service-provider/
-    ├── e2e-service-consumer/
-    └── ...
-```
-
-### E2E Configuration (e2e.yaml)
-
-Each test case has an `e2e.yaml` with four sections:
-
-```yaml
-setup:
-  env: compose                              # Environment: compose or kind (Kubernetes)
-  file: docker-compose.yml                  # Docker compose file
-  timeout: 20m                              # Setup timeout
-  init-system-environment: ../../../script/env  # Shared env variables
-  steps:                                    # Initialization steps
-    - name: install swctl
-      command: bash test/e2e-v2/script/prepare/setup-e2e-shell/install.sh swctl
-
-trigger:
-  action: http                              # Generate test traffic
-  interval: 3s
-  times: -1                                 # -1 = run until verify succeeds
-  url: http://${consumer_host}:${consumer_9092}/users
-  method: POST
-  body: '{"id":"123","name":"skywalking"}'
-
-verify:
-  retry:
-    count: 20
-    interval: 10s
-  cases:
-    - includes:
-        - ../simple-cases.yaml              # Reusable verification cases
-    - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec ...
-      expected: expected/metrics.yml
-
-cleanup:
-  on: always                                # always|success|failure|never
-```
-
-### Running E2E Tests Locally
-
-**Prerequisites:**
-- Docker and Docker Compose
-- Go (for e2e tool installation)
-
-**Quick Start (run simple/jdk test):**
-```bash
-# 1. Build distribution and Docker image
-./mvnw clean package -Pall -Dmaven.test.skip
-make docker
-
-# 2. Build test services
-./mvnw -f test/e2e-v2/java-test-service/pom.xml clean package
-
-# 3. Run e2e test (SW_AGENT_JDK_VERSION is required)
-SW_AGENT_JDK_VERSION=8 e2e run -c test/e2e-v2/cases/simple/jdk/e2e.yaml
-```
-
-**Step-by-step debugging:**
-```bash
-# Set required environment variable
-export SW_AGENT_JDK_VERSION=8
-
-# Run individual steps instead of full test
-e2e setup -c test/e2e-v2/cases/simple/jdk/e2e.yaml    # Start containers
-e2e trigger -c test/e2e-v2/cases/simple/jdk/e2e.yaml  # Generate traffic
-e2e verify -c test/e2e-v2/cases/simple/jdk/e2e.yaml   # Validate results
-e2e cleanup -c test/e2e-v2/cases/simple/jdk/e2e.yaml  # Stop containers
-```
-
-### E2E CLI Commands
-
-| Command | Description |
-|---------|-------------|
-| `e2e run -c <path>` | Run complete test (setup → trigger → verify → cleanup) |
-| `e2e setup -c <path>` | Start containers and initialize environment |
-| `e2e trigger -c <path>` | Generate test traffic |
-| `e2e verify -c <path>` | Validate results against expected output |
-| `e2e cleanup -c <path>` | Stop and remove containers |
-
-### Common Test Cases
-
-| Category | Path | Description |
-|----------|------|-------------|
-| `simple/jdk` | `test/e2e-v2/cases/simple/jdk/` | Basic Java agent with BanyanDB |
-| `storage/banyandb` | `test/e2e-v2/cases/storage/banyandb/` | BanyanDB storage backend |
-| `storage/elasticsearch` | `test/e2e-v2/cases/storage/elasticsearch/` | Elasticsearch storage |
-| `alarm/` | `test/e2e-v2/cases/alarm/` | Alerting functionality |
-| `profiling/trace` | `test/e2e-v2/cases/profiling/trace/` | Trace profiling |
-| `log/` | `test/e2e-v2/cases/log/` | Log analysis (LAL) |
-
-### Writing E2E Tests
-
-1. **Create test directory** under `test/e2e-v2/cases/<category>/<name>/`
-
-2. **Create docker-compose.yml** extending base services:
-   ```yaml
-   version: '2.1'
-   services:
-     oap:
-       extends:
-         file: ../../../script/docker-compose/base-compose.yml
-         service: oap
-     banyandb:
-       extends:
-         file: ../../../script/docker-compose/base-compose.yml
-         service: banyandb
-   ```
-
-3. **Create e2e.yaml** with setup, trigger, verify sections
-
-4. **Create expected/ directory** with expected YAML outputs for verification
-
-5. **Create verification cases** (e.g., `simple-cases.yaml`) with swctl queries
-
-### Verification with swctl
-
-The `swctl` CLI queries OAP's GraphQL API:
-
-```bash
-# Query service metrics
-swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql \
-  metrics exec --expression=service_resp_time --service-name=e2e-service-provider
-
-# List services
-swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql \
-  service ls
-
-# Query traces
-swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql \
-  trace ls --service-name=e2e-service-provider
-```
-
-### Environment Variables
-
-Key version commits in `test/e2e-v2/script/env`:
-- `SW_AGENT_JAVA_COMMIT` - Java agent version
-- `SW_BANYANDB_COMMIT` - BanyanDB version
-- `SW_CTL_COMMIT` - swctl CLI version
-- `SW_AGENT_*_COMMIT` - Other agent versions (Go, Python, NodeJS, PHP)
-
-### Debugging E2E Tests
-
-**If a test fails, do NOT run cleanup immediately.** Keep containers running to debug:
-
-```bash
-# 1. Setup containers (only once)
-e2e setup -c test/e2e-v2/cases/simple/jdk/e2e.yaml
-
-# 2. Generate traffic
-e2e trigger -c test/e2e-v2/cases/simple/jdk/e2e.yaml
-
-# 3. Verify (can re-run multiple times after fixing issues)
-e2e verify -c test/e2e-v2/cases/simple/jdk/e2e.yaml
-
-# Check container logs to debug failures
-docker compose -f test/e2e-v2/cases/simple/jdk/docker-compose.yml logs oap
-docker compose -f test/e2e-v2/cases/simple/jdk/docker-compose.yml logs provider
-
-# Only cleanup when done debugging
-e2e cleanup -c test/e2e-v2/cases/simple/jdk/e2e.yaml
-```
-
-**Determining if rebuild is needed:**
-
-Compare file timestamps against last package build. If any files changed after package, rebuild is needed:
-```bash
-# Find OAP runtime files modified after package was built
-find oap-server apm-protocol -type f \( \
-  -name "*.java" -o -name "*.yaml" -o -name "*.yml" -o \
-  -name "*.json" -o -name "*.xml" -o -name "*.properties" -o \
-  -name "*.proto" \
-\) -newer dist/apache-skywalking-apm-bin.tar.gz 2>/dev/null
-
-# Find test service files modified after last build (needs service rebuild)
-find test/e2e-v2/java-test-service -type f \( \
-  -name "*.java" -o -name "*.xml" -o -name "*.yaml" -o -name "*.yml" \
-\) -newer test/e2e-v2/java-test-service/e2e-service-provider/target/*.jar 2>/dev/null
-
-# Find test case config files modified after package was built
-find test/e2e-v2/cases -type f \( \
-  -name "*.yaml" -o -name "*.yml" -o -name "*.json" \
-\) -newer dist/apache-skywalking-apm-bin.tar.gz 2>/dev/null
-```
-
-Also compare git commit ID in binary vs current HEAD:
-```bash
-# Commit ID in packaged binary
-unzip -p dist/apache-skywalking-apm-bin/oap-libs/server-starter-*.jar version.properties | grep git.commit.id
-
-# Current HEAD
-git rev-parse HEAD
-```
-
-**If rebuild is needed, stop e2e first:**
-```bash
-# 1. Cleanup running containers
-e2e cleanup -c test/e2e-v2/cases/simple/jdk/e2e.yaml
-
-# 2. Rebuild OAP (if oap-server/apm-protocol files changed)
-./mvnw clean package -Pall -Dmaven.test.skip && make docker
-
-# 3. Rebuild test services (if java-test-service files changed)
-./mvnw -f test/e2e-v2/java-test-service/pom.xml clean package
-
-# 4. Restart e2e
-e2e setup -c test/e2e-v2/cases/simple/jdk/e2e.yaml
-e2e trigger -c test/e2e-v2/cases/simple/jdk/e2e.yaml
-e2e verify -c test/e2e-v2/cases/simple/jdk/e2e.yaml
-```
-
-## License Checks (skywalking-eyes)
-
-SkyWalking uses [Apache SkyWalking Eyes](https://github.com/apache/skywalking-eyes) for license header and dependency license checks. **License checks must pass before submitting a PR.**
-
-### Installation
-
-```bash
-# Install the same version used in CI
-go install github.com/apache/skywalking-eyes/cmd/license-eye@5b7ee1731d036b5aac68f8bd3fc9e6f98ada082e
-
-# Or via Homebrew (macOS)
-brew install license-eye
-```
-
-### Commands
-
-```bash
-# Check license headers in source files (fast, run before PR)
-license-eye header check
-
-# Fix missing license headers automatically
-license-eye header fix
-
-# Generate LICENSE file from dependencies
-license-eye dependency resolve --summary ./dist-material/release-docs/LICENSE.tpl
-
-# Check if LICENSE file needs update
-git diff -U0 ./dist-material/release-docs/LICENSE
-```
-
-### Configuration
-
-Configuration is in `.licenserc.yaml`:
-- Defines Apache-2.0 license header
-- Lists paths to ignore (e.g., `**/*.md`, `**/*.json`, generated files)
-- Configures dependency license mappings
-
-### CI Behavior
-
-The CI runs on **Linux (ubuntu-latest)** with two checks:
-1. **license-header**: Verifies all source files have proper Apache-2.0 headers
-2. **dependency-license**: Regenerates LICENSE file and fails if it differs from committed version
-
-**Important:** Some dependencies have platform-specific variants (Windows/macOS suffixes). The LICENSE file should reflect Linux dependencies since CI runs on Linux. If `dependency-license` fails and you're on macOS/Windows, ask maintainers to verify before committing LICENSE changes.
-
 ## Git Submodules
 
 The project uses submodules for protocol definitions and UI:
@@ -586,45 +228,13 @@ Always use `--recurse-submodules` when cloning or update submodules manually.
 
 ## Submitting Pull Requests
 
-### Branch Strategy
-- **Never work directly on master branch**
-- Create a new branch for your changes: `git checkout -b feature/your-feature-name` or `git checkout -b fix/your-fix-name`
-- Keep branch names descriptive and concise
-
-### PR Title
-Summarize the changes in the PR title. Examples:
-- `Fix BanyanDB query timeout issue`
-- `Add support for OpenTelemetry metrics`
-- `Improve documentation structure`
-
-### PR Description
-Follow the PR template in `.github/PULL_REQUEST_TEMPLATE`. Key requirements:
-
-**For Bug Fixes:**
-- Add unit test to verify the fix
-- Explain briefly why the bug exists and how to fix it
-
-**For New Features:**
-- Link to design doc if non-trivial
-- Update documentation
-- Add tests (UT, IT, E2E)
-- Attach screenshots if UI related
-
-**For Performance Improvements:**
-- Add benchmark for the improvement
-- Include benchmark results
-- Link to theory proof or discussion articles
-
-**Always:**
-- Reference related issue: `Closes #<issue number>`
-- Update [`CHANGES` log](https://github.com/apache/skywalking/blob/master/docs/en/changes/changes.md)
-- Add `copilot` as a reviewer for AI-assisted code review
-- Do NOT add AI assistant as co-author. Code responsibility is on the committer's hands.
+Use the `/gh-pull-request` skill for committing and pushing to a PR branch. It runs pre-flight checks (compile, checkstyle, license headers) before every push, and creates the PR if one doesn't exist yet.
 
 ## Tips for AI Assistants
 
 1. **Always check submodules**: Protocol changes may require submodule updates
 2. **Generate sources first**: Run `mvnw compile` before analyzing generated code
+3. **Install package**: Use `mvnw flatten:flatten install` to build the precompiler and export generated classes before running tests. ref to [compile skill doc](.claude/skills/compile/SKILL.md)
 3. **Respect checkstyle**: No System.out, no @author, no Chinese characters
 4. **Follow module patterns**: Use existing modules as templates
 5. **Check multiple storage implementations**: Logic may vary by storage type
@@ -633,4 +243,3 @@ Follow the PR template in `.github/PULL_REQUEST_TEMPLATE`. Key requirements:
 8. **Test both unit and integration**: Different test patterns for different scopes
 9. **Documentation is rendered via markdown**: When reviewing docs, consider how they will be rendered by a markdown engine
 10. **Relative paths in docs are valid**: Relative file paths (e.g., `../../../oap-server/...`) in documentation work both in the repo and on the documentation website, supported by website build tooling
-11. **Read PR template before creating PR**: Always read `.github/PULL_REQUEST_TEMPLATE` and use its exact format with checkboxes, not a custom summary
diff --git a/dist-material/release-docs/LICENSE b/dist-material/release-docs/LICENSE
index a23629e1ed..af44243e39 100644
--- a/dist-material/release-docs/LICENSE
+++ b/dist-material/release-docs/LICENSE
@@ -347,7 +347,6 @@ The text of each license is the standard Apache 2.0 license.
     https://mvnrepository.com/artifact/org.apache.curator/curator-framework/4.3.0 Apache-2.0
     https://mvnrepository.com/artifact/org.apache.curator/curator-recipes/4.3.0 Apache-2.0
     https://mvnrepository.com/artifact/org.apache.curator/curator-x-discovery/4.3.0 Apache-2.0
-    https://mvnrepository.com/artifact/org.apache.groovy/groovy/5.0.3 Apache-2.0
     https://mvnrepository.com/artifact/org.apache.httpcomponents/httpasyncclient/4.1.5 Apache-2.0
     https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient/4.5.13 Apache-2.0
     https://mvnrepository.com/artifact/org.apache.httpcomponents/httpcore/4.4.16 Apache-2.0
diff --git a/docker/.env b/docker/.env
index d4fa1add38..8ca45571cd 100644
--- a/docker/.env
+++ b/docker/.env
@@ -6,6 +6,6 @@
 # docker compose up
 
 ELASTICSEARCH_IMAGE=docker.elastic.co/elasticsearch/elasticsearch-oss:7.4.2
-BANYANDB_IMAGE=ghcr.io/apache/skywalking-banyandb:84f32b3969cdcc676aaee428383b34b3b67dbdf5
+BANYANDB_IMAGE=ghcr.io/apache/skywalking-banyandb:7568a326bb7b10b6aa804bf0f4239904c347d9d5
 OAP_IMAGE=ghcr.io/apache/skywalking/oap:latest
 UI_IMAGE=ghcr.io/apache/skywalking/ui:latest
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index cecefb30d8..7f6e56474d 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -43,7 +43,7 @@ services:
   banyandb:
     profiles:
       - "banyandb"
-    image: ${BANYANDB_IMAGE:-ghcr.io/apache/skywalking-banyandb:a091ac0c3efa7305288ae9fb8853bffb2186583a}
+    image: ${BANYANDB_IMAGE}
     container_name: banyandb
     networks:
       - demo
diff --git a/docs/en/academy/dsl-compiler-design.md b/docs/en/academy/dsl-compiler-design.md
new file mode 100644
index 0000000000..7d415cd8f0
--- /dev/null
+++ b/docs/en/academy/dsl-compiler-design.md
@@ -0,0 +1,163 @@
+# DSL Compiler Design: ANTLR4 + Javassist
+
+## Overview
+
+SkyWalking OAP server uses four domain-specific languages (DSLs) for telemetry analysis.
+All four share the same compilation tech stack: **ANTLR4** for grammar parsing and **Javassist** for
+runtime bytecode generation.
+
+| DSL | Purpose | Input | Generated Output |
+|-----|---------|-------|-----------------|
+| **OAL** (Observability Analysis Language) | Trace/mesh metrics aggregation | `.oal` script files | Metrics classes, builders, dispatchers |
+| **MAL** (Meter Analysis Language) | Meter/metrics expression evaluation | YAML config `exp` fields | `MalExpression` implementations |
+| **LAL** (Log Analysis Language) | Log processing pipelines | YAML config `filter` blocks | `LalExpression` implementations |
+| **Hierarchy Matching Rules** | Service hierarchy relationship matching | YAML config expressions | `BiFunction<Service, Service, Boolean>` implementations |
+
+## Compilation Pipeline
+
+All four DSLs follow the same three-phase compilation pipeline at OAP startup:
+
+```
+DSL string (from .oal script or YAML config)
+    |
+    v
+Phase 1: ANTLR4 Parsing
+    Lexer + Parser (generated from .g4 grammars at build time)
+    → Immutable AST model
+    |
+    v
+Phase 2: Java Source Generation
+    Walk AST model, emit Java source code as strings
+    |
+    v
+Phase 3: Javassist Bytecode Generation
+    ClassPool.makeClass() → CtClass → addMethod(source) → toClass()
+    → Ready-to-use class instance loaded into JVM
+```
+
+### What Each DSL Generates
+
+| DSL | Interface / Base Class | Key Method |
+|-----|----------------------|------------|
+| OAL | Extends metrics function class (e.g., `LongAvgMetrics`) | `id()`, `serialize()`, `deserialize()`, plus dispatcher `dispatch(source)` |
+| MAL metric | `MalExpression` | `SampleFamily run(Map<String, SampleFamily> samples)` |
+| MAL filter | `Predicate<Map<String, String>>` | `boolean test(Map<String, String> tags)` |
+| LAL | `LalExpression` | `void execute(FilterSpec filterSpec, ExecutionContext ctx)` |
+| Hierarchy | `BiFunction<Service, Service, Boolean>` | `Boolean apply(Service upper, Service lower)` |
+
+OAL is the most complex -- it generates **three classes per metric** (metrics class with storage annotations,
+metrics builder for serialization, and source dispatcher for routing), whereas MAL/LAL/Hierarchy each generate
+a single functional class per expression.
+
+## ANTLR4 Grammars
+
+Each DSL has its own ANTLR4 lexer and parser grammar. The Maven ANTLR4 plugin generates Java lexer/parser
+classes at build time; these are then used at runtime to parse DSL strings.
+
+| DSL | Grammar Location |
+|-----|-----------------|
+| OAL | `oap-server/oal-grammar/src/main/antlr4/.../OALLexer.g4`, `OALParser.g4` |
+| MAL | `oap-server/analyzer/meter-analyzer/src/main/antlr4/.../MALLexer.g4`, `MALParser.g4` |
+| LAL | `oap-server/analyzer/log-analyzer/src/main/antlr4/.../LALLexer.g4`, `LALParser.g4` |
+| Hierarchy | `oap-server/analyzer/hierarchy/src/main/antlr4/.../HierarchyRuleLexer.g4`, `HierarchyRuleParser.g4` |
+
+## Javassist Constraints
+
+Javassist compiles Java source strings into bytecode but has limitations that shape the code generation:
+
+- **No anonymous inner classes or lambdas** -- Callback-based APIs require workarounds.
+  LAL uses private methods called directly from `execute()` instead of Consumer callbacks.
+  OAL pre-compiles callbacks as separate `CtClass` instances where needed.
+- **No generics in method bodies** -- Generated source uses raw types with explicit casts.
+- **Class loading anchor** -- Each DSL uses a `PackageHolder` marker class so that
+  `ctClass.toClass(PackageHolder.class)` loads the generated class into the correct module/package
+  (required for JDK 9+ module system).
+
+OAL additionally uses **FreeMarker templates** to generate method bodies for metrics classes, builders, and
+dispatchers, since these classes are more complex and benefit from template-driven generation.
+
+## Module Structure
+
+```
+oap-server/
+  oal-grammar/            # OAL: ANTLR4 grammar
+  oal-rt/                 # OAL: compiler + runtime (Javassist + FreeMarker)
+  analyzer/
+    meter-analyzer/       # MAL: grammar + compiler + runtime
+    log-analyzer/         # LAL: grammar + compiler + runtime
+    hierarchy/            # Hierarchy: grammar + compiler + runtime
+    agent-analyzer/       # Calls MAL compiler for meter data
+```
+
+OAL keeps grammar and runtime in separate modules (`oal-grammar` and `oal-rt`) because `server-core`
+depends on the grammar while the runtime implementation depends on `server-core` (avoiding circular
+dependency). MAL, LAL, and Hierarchy are each self-contained in a single module.
+
+## Groovy Replacement (MAL, LAL, Hierarchy)
+
+Reference: [Discussion #13716](https://github.com/apache/skywalking/discussions/13716)
+
+MAL, LAL, and Hierarchy previously used **Groovy** as the runtime scripting engine. OAL has always used
+ANTLR4 + Javassist. The Groovy-based DSLs were replaced for the following reasons:
+
+1. **Startup cost** -- 1,250+ `GroovyShell.parse()` calls at OAP boot, each spinning up the full Groovy
+   compiler pipeline.
+
+2. **Runtime execution overhead** -- MAL expressions execute on every metrics ingestion cycle. Per-expression
+   overhead from dynamic Groovy compounds at scale: property resolution through 4+ layers of indirection,
+   `ExpandoMetaClass` closure allocation for simple arithmetic, and megamorphic call sites that defeat JIT
+   optimization.
+
+3. **Late error detection** -- MAL uses dynamic Groovy; typos in metric names or invalid method chains are
+   only discovered when that specific expression runs with real data.
+
+4. **Debugging complexity** -- Stack traces include Groovy MOP internals (`CallSite`, `MetaClassImpl`,
+   `ExpandoMetaClass`), obscuring the actual expression logic.
+
+5. **GraalVM incompatibility** -- `invokedynamic` bootstrapping and `ExpandoMetaClass` are fundamentally
+   incompatible with ahead-of-time (AOT) compilation, blocking the
+   [GraalVM native-image distribution](https://github.com/apache/skywalking-graalvm-distro).
+
+The DSL grammar for users remains **100% unchanged** -- the same expressions written in YAML config files
+work exactly as before. Only the internal compilation engine was replaced.
+
+### Verification: Groovy v1 Checker
+
+To ensure the new Java compilers produce identical results to the original Groovy implementation,
+a **dual-path comparison test suite** is maintained under `test/script-cases/`:
+
+```
+test/script-cases/
+  scripts/
+    mal/                      # Copies of shipped MAL configs (test-otel-rules, test-meter-analyzer-config, etc.)
+    lal/                      # Copies of shipped LAL scripts (test-lal/)
+    hierarchy-rule/           # Copy of shipped hierarchy-definition.yml
+  script-runtime-with-groovy/
+    mal-v1-with-groovy/       # MAL v1: original Groovy-based implementation
+    lal-v1-with-groovy/       # LAL v1: original Groovy-based implementation
+    hierarchy-v1-with-groovy/ # Hierarchy v1: original Groovy-based implementation
+    mal-lal-v1-v2-checker/    # Runs every MAL/LAL expression through BOTH v1 and v2, compares results
+    hierarchy-v1-v2-checker/  # Runs every hierarchy rule through BOTH v1 and v2, compares results
+```
+
+The checker mechanism:
+
+1. Loads all test copies of production YAML config files from `test/script-cases/scripts/`
+2. For each DSL expression, compiles with **both** v1 (Groovy) and v2 (ANTLR4 + Javassist)
+3. Compares the results:
+   - **MAL**: Two-level comparison for each expression:
+     1. **Metadata comparison** -- sample names, aggregation labels, downsampling type, percentile config
+     2. **Runtime execution comparison** -- builds mock `SampleFamily` input data from `ExpressionMetadata`,
+        executes with both v1 and v2, compares output samples (count, labels, values with epsilon).
+        For `increase()`/`rate()` expressions, the `CounterWindow` is primed with an initial run before
+        comparing the second run's output.
+   - **LAL**: Runtime execution comparison -- both v1 and v2 execute with mock LogData,
+     then compare execution state (service, layer, tags, abort/save flags).
+     For rules requiring extraLog (e.g., envoy-als), mock proto data is built from `.input.data` files
+     and the `LALSourceTypeProvider` SPI resolves the proto type per layer.
+     Test scripts include both copies of production configs (`oap-cases/`) and
+     dedicated feature-coverage rules (`feature-cases/`).
+   - **Hierarchy**: Compare `BiFunction` evaluation with test Service pairs
+
+This ensures 100% behavioral parity. The Groovy v1 modules are **test-only dependencies** -- they are not
+included in the OAP distribution.
\ No newline at end of file
diff --git a/docs/en/changes/changes.md b/docs/en/changes/changes.md
index 5752f12b6d..47cc56f67b 100644
--- a/docs/en/changes/changes.md
+++ b/docs/en/changes/changes.md
@@ -7,9 +7,18 @@
   - Precise error location reporting with file, line, and column numbers
   - Clean separation between parsing and code generation phases
   - Enhanced testability with models that can be constructed without parsing
+* Introduce MAL/LAL/Hierarchy V2 engine — replace Groovy-based DSL runtime with ANTLR4 parser + Javassist bytecode generation:
+  - Remove Groovy runtime dependency from OAP backend
+  - Fail-fast compilation at startup — syntax and type errors are caught immediately instead of at first execution
+  - Thread-safe generated classes with no ThreadLocal or shared mutable state
+  - Immutable AST models for all three DSLs (MAL, LAL, Hierarchy rules)
+  - Explicit context passing replaces Groovy binding/closure capture
+  - v1 (Groovy) and v2 (ANTLR4+Javassist) cross-version checker validates behavioral equivalence across 1,290+ expressions
+  - JMH benchmarks confirm v2 runtime speedups: MAL execute ~6.8x, LAL compile ~39x / execute ~2.8x, Hierarchy execute ~2.6x faster than Groovy v1
 * Fix E2E test metrics verify: make it failure if the metric values all null.
 * Support building, testing, and publishing with Java 25.
 * Add `CLAUDE.md` as AI assistant guide for the project.
+* Upgrade Byte Buddy to 1.18.7 and configure explicit `-javaagent` for Mockito/Byte Buddy in Surefire to avoid JDK 25 dynamic agent loading warnings.
 * Upgrade Groovy to 5.0.3 in OAP backend.
 * Bump up nodejs to v24.13.0 for the latest UI(booster-ui) compiling.
 * Drop Elasticsearch 7.x (EOL) and OpenSearch 1.x from E2E tests, upgrade all ES tests to 8.18.8, and update skywalking-helm to use ECK 8.18.8.
@@ -69,6 +78,7 @@
 * Replace PowerMock Whitebox with standard Java Reflection in `server-library`, `server-core`, and `server-configuration` to support JDK 25+.
 * Fix `/debugging/config/dump` may leak sensitive information if there are second level properties in the configuration.
 
+
 #### OAP Server
 
 * KubernetesCoordinator: make self instance return real pod IP address instead of `127.0.0.1`.
@@ -136,7 +146,10 @@
 * Add the spring-ai components and the GenAI layer.
 * Bump up netty to 4.2.10.Final.
 * Bump up log4j to 2.25.3 and jackson to 2.18.5.
+* Remove PowerMock dependency. Replace `Whitebox` with `ReflectUtil` (standard Java reflection + `sun.misc.Unsafe` for final fields) across all modules to support JDK 25+.
 * Support TraceQL and Tempo API for Zipkin trace query.
+* Remove `initExp` from MAL configuration. It was an internal Groovy startup validation mechanism, not an end-user feature. The v2 ANTLR4 compiler performs fail-fast validation at startup natively.
+* Update hierarchy rule documentation: `auto-matching-rules` in `hierarchy-definition.yml` no longer use Groovy scripts. Rules now use a dedicated expression grammar supporting property access, String methods, if/else, comparisons, and logical operators. All shipped rules are fully compatible.
 
 #### UI
 * Fix the missing icon in new native trace view.
diff --git a/docs/en/concepts-and-designs/lal.md b/docs/en/concepts-and-designs/lal.md
index f843871cf8..4dff550d3d 100644
--- a/docs/en/concepts-and-designs/lal.md
+++ b/docs/en/concepts-and-designs/lal.md
@@ -31,7 +31,7 @@ are cases where you may want the filter chain to stop earlier when specified con
 the remaining filter chain from where it's declared, and all the remaining components won't be executed at all.
 `abort` function serves as a fast-fail mechanism in LAL.
 
-```groovy
+```
 filter {
     if (log.service == "TestingService") { // Don't waste resources on TestingServices
         abort {} // all remaining components won't be executed at all
@@ -67,7 +67,7 @@ We can add tags like following:
 ]
 ``` 
 And we can use this method to get the value of the tag key `TEST_KEY`.
-```groovy
+```
 filter {
     if (tag("TEST_KEY") == "TEST_VALUE") {
          ...   
@@ -95,7 +95,7 @@ See examples below.
 
 #### `json`
 
-```groovy
+```
 filter {
     json {
         abortOnFailure true // this is optional because it's default behaviour
@@ -105,7 +105,7 @@ filter {
 
 #### `yaml`
 
-```groovy
+```
 filter {
     yaml {
         abortOnFailure true // this is optional because it's default behaviour
@@ -123,7 +123,7 @@ For unstructured logs, there are some `text` parsers for use.
 all the captured groups can be used later in the extractors or sinks.
 `regexp` returns a `boolean` indicating whether the log matches the pattern or not.
 
-```groovy
+```
 filter {
     text {
         abortOnFailure true // this is optional because it's default behaviour
@@ -181,7 +181,7 @@ dropped) and is used to associate with traces / metrics.
 not dropped) and is used to associate with traces / metrics.
 
 The parameter of `timestamp` can be a millisecond:
-```groovy
+```
 filter {
     // ... parser
 
@@ -191,7 +191,7 @@ filter {
 }
 ```
 or a datetime string with a specified pattern:
-```groovy
+```
 filter {
     // ... parser
 
@@ -210,9 +210,7 @@ not dropped) and is used to associate with service.
 
 `tag` extracts the tags from the `parsed` result, and set them into the `LogData`. The form of this extractor should look something like this: `tag key1: value, key2: value2`. You may use the properties of `parsed` as both keys and values.
 
-```groovy
-import javax.swing.text.LayeredHighlighter
-
+```
 filter {
     // ... parser
 
@@ -242,7 +240,7 @@ log-analyzer:
 
 Examples are as follows:
 
-```groovy
+```
 filter {
     // ...
     extractor {
@@ -338,7 +336,7 @@ dropped) and is used to associate with TopNDatabaseStatement.
 
 An example of LAL to distinguish slow logs:
 
-```groovy
+```
 filter {
   json{
   }
@@ -386,7 +384,7 @@ An example of JSON sent to OAP is as following:
 ```
 Examples are as follows:
 
-```groovy
+```
 filter {
     json {
     }
@@ -447,7 +445,7 @@ final sampling result. See examples in [Enforcer](#enforcer).
 
 Examples 1, `rateLimit`:
 
-```groovy
+```
 filter {
     // ... parser
 
@@ -469,7 +467,7 @@ filter {
 
 Examples 2, `possibility`:
 
-```groovy
+```
 filter {
     // ... parser
 
@@ -492,7 +490,7 @@ filter {
 Dropper is a special sink, meaning that all logs are dropped without any exception. This is useful when you want to
 drop debugging logs.
 
-```groovy
+```
 filter {
     // ... parser
 
@@ -510,7 +508,7 @@ filter {
 
 Or if you have multiple filters, some of which are for extracting metrics, only one of them has to be persisted.
 
-```groovy
+```
 filter { // filter A: this is for persistence
     // ... parser
 
@@ -539,7 +537,7 @@ Enforcer is another special sink that forcibly samples the log. A typical use ca
 configured a sampler and want to save some logs forcibly, such as to save error logs even if the sampling mechanism
 has been configured.
 
-```groovy
+```
 filter {
     // ... parser
 
diff --git a/docs/en/concepts-and-designs/mal.md b/docs/en/concepts-and-designs/mal.md
index 32f4b23199..a6f0420f5e 100644
--- a/docs/en/concepts-and-designs/mal.md
+++ b/docs/en/concepts-and-designs/mal.md
@@ -294,8 +294,6 @@ Generic placeholders are defined as follows:
 * `<closure>`: A closure with custom logic.
 
 ```yaml
-# initExp is the expression that initializes the current configuration file
-initExp: <string>
 # filter the metrics, only those metrics that satisfy this condition will be passed into the `metricsRules` below.
 filter: <closure> # example: '{ tags -> tags.job_name == "vm-monitoring" }'
 # expPrefix is executed before the metrics executes other functions.
diff --git a/docs/en/concepts-and-designs/oal.md b/docs/en/concepts-and-designs/oal.md
index ce035f225a..e0becc4dbf 100644
--- a/docs/en/concepts-and-designs/oal.md
+++ b/docs/en/concepts-and-designs/oal.md
@@ -23,7 +23,8 @@ However, the OAL script is a compiled language, and the OAL Runtime generates ja
 the changes of those scripts in the runtime.
 If your OAP servers are running in a cluster mode, these script defined metrics should be aligned.
 
-You can set `SW_OAL_ENGINE_DEBUG=Y` at system env to see which classes are generated.
+You can set `SW_DYNAMIC_CLASS_ENGINE_DEBUG=Y` at system env to dump generated `.class` files to disk.
+See [Dynamic Code Generation and Debugging](../operation/dynamic-code-generation-debugging.md) for details.
 
 ## Grammar
 Scripts should be named `*.oal`
diff --git a/docs/en/concepts-and-designs/service-hierarchy-configuration.md b/docs/en/concepts-and-designs/service-hierarchy-configuration.md
index 6aa0d40d56..7aac0e99fa 100644
--- a/docs/en/concepts-and-designs/service-hierarchy-configuration.md
+++ b/docs/en/concepts-and-designs/service-hierarchy-configuration.md
@@ -69,7 +69,7 @@ layer-levels:
 
 ### Auto Matching Rules
 - The auto matching rules are defined in the `auto-matching-rules` section.
-- Use Groovy script to define the matching rules, the input parameters are the upper service(u) and the lower service(l) and the return value is a boolean, 
+- The matching rules are expressions where the input parameters are the upper service(u) and the lower service(l) and the return value is a boolean,
 which are used to match the relation between the upper service(u) and the lower service(l) on the different layers.
 - The default matching rules required the service name configured as SkyWalking default and follow the [Showcase](https://github.com/apache/skywalking-showcase).
 If you customized the service name in any layer, you should customize the related matching rules according your service name rules.
diff --git a/docs/en/concepts-and-designs/service-hierarchy.md b/docs/en/concepts-and-designs/service-hierarchy.md
index 8cd18bccda..5f3c5144fc 100644
--- a/docs/en/concepts-and-designs/service-hierarchy.md
+++ b/docs/en/concepts-and-designs/service-hierarchy.md
@@ -45,7 +45,7 @@ If you want to customize it according to your own needs, please refer to [Servic
 
 #### GENERAL On K8S_SERVICE
 - Rule name: `lower-short-name-remove-ns`
-- Groovy script: `{ (u, l) -> u.shortName == l.shortName.substring(0, l.shortName.lastIndexOf('.')) }`
+- Matching expression: `{ (u, l) -> u.shortName == l.shortName.substring(0, l.shortName.lastIndexOf('.')) }`
 - Description: GENERAL.service.shortName == K8S_SERVICE.service.shortName without namespace
 - Matched Example:
   - GENERAL.service.name: `agent::songs`
@@ -53,7 +53,7 @@ If you want to customize it according to your own needs, please refer to [Servic
 
 #### GENERAL On APISIX
 - Rule name: `lower-short-name-remove-ns`
-- Groovy script: `{ (u, l) -> u.shortName == l.shortName.substring(0, l.shortName.lastIndexOf('.')) }`
+- Matching expression: `{ (u, l) -> u.shortName == l.shortName.substring(0, l.shortName.lastIndexOf('.')) }`
 - Description: GENERAL.service.shortName == APISIX.service.shortName without namespace
 - Matched Example:
   - GENERAL.service.name: `agent::frontend`
@@ -62,7 +62,7 @@ If you want to customize it according to your own needs, please refer to [Servic
 
 #### VIRTUAL_DATABASE On MYSQL
 - Rule name: `lower-short-name-with-fqdn`
-- Groovy script: `{ (u, l) -> u.shortName.substring(0, u.shortName.lastIndexOf(':')) == l.shortName.concat('.svc.cluster.local') }`
+- Matching expression: `{ (u, l) -> u.shortName.substring(0, u.shortName.lastIndexOf(':')) == l.shortName.concat('.svc.cluster.local') }`
 - Description: VIRTUAL_DATABASE.service.shortName remove port == MYSQL.service.shortName with fqdn suffix
 - Matched Example:
   - VIRTUAL_DATABASE.service.name: `mysql.skywalking-showcase.svc.cluster.local:3306`
@@ -70,7 +70,7 @@ If you want to customize it according to your own needs, please refer to [Servic
 
 #### VIRTUAL_DATABASE On POSTGRESQL
 - Rule name: `lower-short-name-with-fqdn`
-- Groovy script: `{ (u, l) -> u.shortName.substring(0, u.shortName.lastIndexOf(':')) == l.shortName.concat('.svc.cluster.local') }`
+- Matching expression: `{ (u, l) -> u.shortName.substring(0, u.shortName.lastIndexOf(':')) == l.shortName.concat('.svc.cluster.local') }`
 - Description: VIRTUAL_DATABASE.service.shortName remove port == POSTGRESQL.service.shortName with fqdn suffix
 - Matched Example:
   - VIRTUAL_DATABASE.service.name: `psql.skywalking-showcase.svc.cluster.local:5432`
@@ -78,7 +78,7 @@ If you want to customize it according to your own needs, please refer to [Servic
 
 #### VIRTUAL_DATABASE On CLICKHOUSE
 - Rule name: `lower-short-name-with-fqdn`
-- Groovy script: `{ (u, l) -> u.shortName.substring(0, u.shortName.lastIndexOf(':')) == l.shortName.concat('.svc.cluster.local') }`
+- Matching expression: `{ (u, l) -> u.shortName.substring(0, u.shortName.lastIndexOf(':')) == l.shortName.concat('.svc.cluster.local') }`
 - Description: VIRTUAL_DATABASE.service.shortName remove port == CLICKHOUSE.service.shortName with fqdn suffix
 - Matched Example:
   - VIRTUAL_DATABASE.service.name: `clickhouse.skywalking-showcase.svc.cluster.local:8123`
@@ -87,7 +87,7 @@ If you want to customize it according to your own needs, please refer to [Servic
 
 #### VIRTUAL_MQ On ROCKETMQ
 - Rule name: `lower-short-name-with-fqdn`
-- Groovy script: `{ (u, l) -> u.shortName.substring(0, u.shortName.lastIndexOf(':')) == l.shortName.concat('.svc.cluster.local') }`
+- Matching expression: `{ (u, l) -> u.shortName.substring(0, u.shortName.lastIndexOf(':')) == l.shortName.concat('.svc.cluster.local') }`
 - Description: VIRTUAL_MQ.service.shortName remove port == ROCKETMQ.service.shortName with fqdn suffix
 - Matched Example:
   - VIRTUAL_MQ.service.name: `rocketmq.skywalking-showcase.svc.cluster.local:9876`
@@ -95,7 +95,7 @@ If you want to customize it according to your own needs, please refer to [Servic
 
 #### VIRTUAL_MQ On RABBITMQ
 - Rule name: `lower-short-name-with-fqdn`
-- Groovy script: `{ (u, l) -> u.shortName.substring(0, u.shortName.lastIndexOf(':')) == l.shortName.concat('.svc.cluster.local') }`
+- Matching expression: `{ (u, l) -> u.shortName.substring(0, u.shortName.lastIndexOf(':')) == l.shortName.concat('.svc.cluster.local') }`
 - Description: VIRTUAL_MQ.service.shortName remove port == RABBITMQ.service.shortName with fqdn suffix
 - Matched Example:
   - VIRTUAL_MQ.service.name: `rabbitmq.skywalking-showcase.svc.cluster.local:5672`
@@ -103,7 +103,7 @@ If you want to customize it according to your own needs, please refer to [Servic
   -
 #### VIRTUAL_MQ On KAFKA
 - Rule name: `lower-short-name-with-fqdn`
-- Groovy script: `{ (u, l) -> u.shortName.substring(0, u.shortName.lastIndexOf(':')) == l.shortName.concat('.svc.cluster.local') }`
+- Matching expression: `{ (u, l) -> u.shortName.substring(0, u.shortName.lastIndexOf(':')) == l.shortName.concat('.svc.cluster.local') }`
 - Description: VIRTUAL_MQ.service.shortName remove port == KAFKA.service.shortName with fqdn suffix
 - Matched Example:
   - VIRTUAL_MQ.service.name: `kafka.skywalking-showcase.svc.cluster.local:9092`
@@ -111,7 +111,7 @@ If you want to customize it according to your own needs, please refer to [Servic
 
 #### VIRTUAL_MQ On PULSAR
 - Rule name: `lower-short-name-with-fqdn`
-- Groovy script: `{ (u, l) -> u.shortName.substring(0, u.shortName.lastIndexOf(':')) == l.shortName.concat('.svc.cluster.local') }`
+- Matching expression: `{ (u, l) -> u.shortName.substring(0, u.shortName.lastIndexOf(':')) == l.shortName.concat('.svc.cluster.local') }`
 - Description: VIRTUAL_MQ.service.shortName remove port == PULSAR.service.shortName with fqdn suffix
 - Matched Example:
   - VIRTUAL_MQ.service.name: `pulsar.skywalking-showcase.svc.cluster.local:6650`
@@ -119,7 +119,7 @@ If you want to customize it according to your own needs, please refer to [Servic
 
 #### MESH On MESH_DP
 - Rule name: `name` 
-- Groovy script: `{ (u, l) -> u.name == l.name }`
+- Matching expression: `{ (u, l) -> u.name == l.name }`
 - Description: MESH.service.name == MESH_DP.service.name
 - Matched Example: 
     - MESH.service.name: `mesh-svr::songs.sample-services`
@@ -127,7 +127,7 @@ If you want to customize it according to your own needs, please refer to [Servic
 
 #### MESH On K8S_SERVICE
 - Rule name: `short-name`
-- Groovy script: `{ (u, l) -> u.shortName == l.shortName }`
+- Matching expression: `{ (u, l) -> u.shortName == l.shortName }`
 - Description: MESH.service.shortName == K8S_SERVICE.service.shortName
 - Matched Example: 
     - MESH.service.name: `mesh-svr::songs.sample-services`
@@ -135,7 +135,7 @@ If you want to customize it according to your own needs, please refer to [Servic
 
 #### MESH_DP On K8S_SERVICE
 - Rule name: `short-name`
-- Groovy script: `{ (u, l) -> u.shortName == l.shortName }`
+- Matching expression: `{ (u, l) -> u.shortName == l.shortName }`
 - Description: MESH_DP.service.shortName == K8S_SERVICE.service.shortName
 - Matched Example: 
     - MESH_DP.service.name: `mesh-svr::songs.sample-services`
@@ -143,7 +143,7 @@ If you want to customize it according to your own needs, please refer to [Servic
 
 #### MYSQL On K8S_SERVICE
 - Rule name: `short-name`
-- Groovy script: `{ (u, l) -> u.shortName == l.shortName }`
+- Matching expression: `{ (u, l) -> u.shortName == l.shortName }`
 - Description: MYSQL.service.shortName == K8S_SERVICE.service.shortName
 - Matched Example: 
     - MYSQL.service.name: `mysql::mysql.skywalking-showcase`
@@ -151,7 +151,7 @@ If you want to customize it according to your own needs, please refer to [Servic
 
 #### POSTGRESQL On K8S_SERVICE
 - Rule name: `short-name`
-- Groovy script: `{ (u, l) -> u.shortName == l.shortName }`
+- Matching expression: `{ (u, l) -> u.shortName == l.shortName }`
 - Description: POSTGRESQL.service.shortName == K8S_SERVICE.service.shortName
 - Matched Example: 
     - POSTGRESQL.service.name: `postgresql::psql.skywalking-showcase`
@@ -159,7 +159,7 @@ If you want to customize it according to your own needs, please refer to [Servic
 
 #### CLICKHOUSE On K8S_SERVICE
 - Rule name: `short-name`
-- Groovy script: `{ (u, l) -> u.shortName == l.shortName }`
+- Matching expression: `{ (u, l) -> u.shortName == l.shortName }`
 - Description: CLICKHOUSE.service.shortName == K8S_SERVICE.service.shortName
 - Matched Example:
   - CLICKHOUSE.service.name: `clickhouse::clickhouse.skywalking-showcase`
@@ -167,7 +167,7 @@ If you want to customize it according to your own needs, please refer to [Servic
 
 #### NGINX On K8S_SERVICE
 - Rule name: `short-name`
-- Groovy script: `{ (u, l) -> u.shortName == l.shortName }`
+- Matching expression: `{ (u, l) -> u.shortName == l.shortName }`
 - Description: NGINX.service.shortName == K8S_SERVICE.service.shortName
 - Matched Example: 
     - NGINX.service.name: `nginx::nginx.skywalking-showcase`
@@ -175,7 +175,7 @@ If you want to customize it according to your own needs, please refer to [Servic
 
 #### APISIX On K8S_SERVICE
 - Rule name: `short-name`
-- Groovy script: `{ (u, l) -> u.shortName == l.shortName }`
+- Matching expression: `{ (u, l) -> u.shortName == l.shortName }`
 - Description: APISIX.service.shortName == K8S_SERVICE.service.shortName
 - Matched Example: 
     - APISIX.service.name: `APISIX::frontend.sample-services`
@@ -183,7 +183,7 @@ If you want to customize it according to your own needs, please refer to [Servic
 
 #### ROCKETMQ On K8S_SERVICE
 - Rule name: `short-name`
-- Groovy script: `{ (u, l) -> u.shortName == l.shortName }`
+- Matching expression: `{ (u, l) -> u.shortName == l.shortName }`
 - Description: ROCKETMQ.service.shortName == K8S_SERVICE.service.shortName
 - Matched Example:
   - ROCKETMQ.service.name: `rocketmq::rocketmq.skywalking-showcase`
@@ -191,7 +191,7 @@ If you want to customize it according to your own needs, please refer to [Servic
 
 #### RABBITMQ On K8S_SERVICE
 - Rule name: `short-name`
-- Groovy script: `{ (u, l) -> u.shortName == l.shortName }`
+- Matching expression: `{ (u, l) -> u.shortName == l.shortName }`
 - Description: RABBITMQ.service.shortName == K8S_SERVICE.service.shortName
 - Matched Example:
   - RABBITMQ.service.name: `rabbitmq::rabbitmq.skywalking-showcase`
@@ -199,7 +199,7 @@ If you want to customize it according to your own needs, please refer to [Servic
 
 #### KAFKA On K8S_SERVICE
 - Rule name: `short-name`
-- Groovy script: `{ (u, l) -> u.shortName == l.shortName }`
+- Matching expression: `{ (u, l) -> u.shortName == l.shortName }`
 - Description: KAFKA.service.shortName == K8S_SERVICE.service.shortName
 - Matched Example:
   - KAFKA.service.name: `kafka::kafka.skywalking-showcase`
@@ -207,7 +207,7 @@ If you want to customize it according to your own needs, please refer to [Servic
 
 #### PULSAR On K8S_SERVICE
 - Rule name: `short-name`
-- Groovy script: `{ (u, l) -> u.shortName == l.shortName }`
+- Matching expression: `{ (u, l) -> u.shortName == l.shortName }`
 - Description: PULSAR.service.shortName == K8S_SERVICE.service.shortName
 - Matched Example:
   - PULSAR.service.name: `pulsar::pulsar.skywalking-showcase`
@@ -215,7 +215,7 @@ If you want to customize it according to your own needs, please refer to [Servic
 
 #### SO11Y_OAP On K8S_SERVICE
 - Rule name: `short-name`
-- Groovy script: `{ (u, l) -> u.shortName == l.shortName }`
+- Matching expression: `{ (u, l) -> u.shortName == l.shortName }`
 - Description: SO11Y_OAP.service.shortName == K8S_SERVICE.service.shortName
 - Matched Example:
   - SO11Y_OAP.service.name: `demo-oap.skywalking-showcase`
@@ -223,7 +223,7 @@ If you want to customize it according to your own needs, please refer to [Servic
 
 #### KONG On K8S_SERVICE
 - Rule name: `short-name`
-- Groovy script: `{ (u, l) -> u.shortName == l.shortName }`
+- Matching expression: `{ (u, l) -> u.shortName == l.shortName }`
 - Description: KONG.service.shortName == K8S_SERVICE.service.shortName
 - Matched Example:
   - KONG.service.name: `kong::kong.skywalking-showcase`
diff --git a/docs/en/guides/claude-code-skills.md b/docs/en/guides/claude-code-skills.md
new file mode 100644
index 0000000000..93c6cb012f
--- /dev/null
+++ b/docs/en/guides/claude-code-skills.md
@@ -0,0 +1,29 @@
+# Claude Code Skills for SkyWalking Development
+
+[Claude Code](https://docs.anthropic.com/en/docs/claude-code) is Anthropic's CLI tool for AI-assisted coding.
+This project provides a set of Claude Code skills (slash commands) that automate common development workflows.
+
+**Note**: These skills are specific to Claude Code. They are defined in the `.claude/skills/` directory
+and are not recognized by other AI coding tools.
+
+## Available Skills
+
+| Skill | Command | Description |
+|-------|---------|-------------|
+| Compile | `/compile [all\|backend\|javadoc\|checkstyle\|module-name]` | Build the OAP server, run javadoc checks, or verify checkstyle |
+| Test | `/test [unit\|integration\|slow\|module-name]` | Run unit tests, integration tests, or slow integration tests matching CI |
+| License | `/license [check\|fix\|deps]` | Check and fix Apache 2.0 license headers and dependency licenses |
+| Pull Request | `/gh-pull-request` | Commit, push, and create a PR with pre-flight checks (compile, checkstyle, license headers) |
+| E2E Debug | `/ci-e2e-debug <RUN_ID or URL>` | Download and inspect CI e2e test logs from GitHub Actions artifacts |
+| Generate Classes | `/generate-classes <mal\|oal\|lal\|hierarchy\|all>` | Generate bytecode classes from DSL scripts for inspection |
+| Run E2E | `/run-e2e [test-case-path]` | Run SkyWalking e2e tests locally |
+
+## Typical Workflow
+
+1. Make your code changes.
+2. Run `/compile` to verify the build passes.
+3. Run `/test` to run relevant tests.
+4. Run `/license check` to verify license headers.
+5. Run `/gh-pull-request` to commit, push, and open a PR.
+
+If a CI e2e test fails after pushing, use `/ci-e2e-debug <RUN_ID>` to download and inspect the logs.
diff --git a/docs/en/operation/dynamic-code-generation-debugging.md b/docs/en/operation/dynamic-code-generation-debugging.md
new file mode 100644
index 0000000000..e4a8ff838e
--- /dev/null
+++ b/docs/en/operation/dynamic-code-generation-debugging.md
@@ -0,0 +1,187 @@
+# Dynamic Code Generation and Debugging
+
+SkyWalking OAP server uses four Domain-Specific Languages (DSLs) to define observability logic:
+**OAL** (traces/mesh metrics), **MAL** (meter metrics), **LAL** (log analysis), and **Hierarchy** (service matching rules).
+These DSL scripts are compiled into JVM bytecode when the OAP server starts.
+The generated classes run in-process — there are no intermediate source files.
+
+When a runtime error occurs inside these generated classes, the stack trace references class names and
+source locations that map back to the original DSL configuration files.
+This document explains how to dump the generated bytecode to disk for inspection and how to read the
+error messages.
+
+## DSL Configuration Files
+
+| DSL | Config Location | What It Generates |
+|-----|----------------|-------------------|
+| OAL | `config/*.oal` | Metrics, MetricsBuilder, and Dispatcher classes per metric definition |
+| MAL | `config/meter-analyzer-config/*.yaml`, `config/otel-rules/**`, `config/envoy-metrics-rules/*.yaml` | One class per metric expression |
+| LAL | `config/lal/*.yaml` | One class per log filter rule |
+| Hierarchy | `config/hierarchy-definition.yml` | One class per auto-matching rule |
+
+All paths are relative to the OAP distribution root directory.
+
+## Dumping Generated Classes
+
+Set the environment variable `SW_DYNAMIC_CLASS_ENGINE_DEBUG` to any non-empty value before starting the OAP server.
+All four DSL compilers check this variable and dump `.class` files to disk when it is set.
+
+```shell
+# Binary distribution
+export SW_DYNAMIC_CLASS_ENGINE_DEBUG=Y
+bin/oapService.sh
+```
+
+```shell
+# Docker
+docker run -e SW_DYNAMIC_CLASS_ENGINE_DEBUG=Y ... apache/skywalking-oap-server
+```
+
+```yaml
+# Kubernetes (in container env section)
+env:
+  - name: SW_DYNAMIC_CLASS_ENGINE_DEBUG
+    value: "Y"
+```
+
+### Output Directory Structure
+
+The generated `.class` files are written to sibling directories next to `oap-libs/`:
+
+**Binary distribution** (`apache-skywalking-apm-bin/`):
+```
+apache-skywalking-apm-bin/
+├── config/                   ← DSL source scripts (*.oal, *.yaml, *.yml)
+├── oap-libs/                 ← OAP server jars
+├── oal-rt/                   ← Generated OAL classes
+│   ├── metrics/              ←   e.g., ServiceRespTimeMetrics.class
+│   ├── metrics/builder/      ←   e.g., ServiceRespTimeMetricsBuilder.class
+│   └── dispatcher/           ←   e.g., ServiceDispatcher.class
+├── mal-rt/                   ← Generated MAL classes (e.g., meter_vm_cpu_total_percentage.class)
+├── lal-rt/                   ← Generated LAL classes (e.g., default_default.class)
+└── hierarchy-rt/             ← Generated Hierarchy classes (e.g., name.class)
+```
+
+**Docker** (`/skywalking/`):
+```
+/skywalking/
+├── config/
+├── oap-libs/
+├── oal-rt/
+├── mal-rt/
+├── lal-rt/
+└── hierarchy-rt/
+```
+
+The OAL output directories are cleaned on each restart. MAL, LAL, and Hierarchy directories are created
+on demand if they don't exist.
+
+### Inspecting Generated Classes
+
+Use `javap` to decompile a generated `.class` file:
+
+```shell
+javap -v -p oal-rt/metrics/ServiceRespTimeMetrics.class
+```
+
+The output includes:
+- **SourceFile attribute** — shows the DSL source file and the generated class name.
+- **LineNumberTable** — maps bytecode offsets to statement numbers, used by the JVM in stack traces.
+- **LocalVariableTable** — shows named local variables for readability.
+
+## Reading Error Stack Traces
+
+When a runtime error occurs inside a generated class, the JVM prints a stack trace that combines the
+`SourceFile` attribute and the `LineNumberTable`. The format is:
+
+```
+at <package>.<ClassName>.<method>(SourceFile:LineNumber)
+```
+
+The `SourceFile` attribute encodes the original DSL configuration file in parentheses:
+
+```
+(<dsl_source_file>:<rule_line_or_index>)<GeneratedClassName>.java
+```
+
+### Example Stack Trace
+
+```
+java.lang.ArithmeticException: / by zero
+    at ...metrics.generated.ServiceRespTimeMetrics.id0((core.oal:20)ServiceRespTimeMetrics.java:3)
+    at ...worker.MetricsStreamProcessor.in(MetricsStreamProcessor.java:...)
+    ...
+```
+
+Reading this:
+- `(core.oal:20)` — the error originates from OAL file `core.oal`, line 20
+- `ServiceRespTimeMetrics.java` — the generated class for metric `ServiceRespTime`
+- `:3` — statement 3 within the generated `id0` method
+
+### Format Per DSL
+
+| DSL | SourceFile Example | How to Read |
+|-----|-------------------|-------------|
+| OAL | `(core.oal:20)ServiceRespTimeMetrics.java` | OAL file `core.oal`, line 20 defines this metric |
+| MAL | `(spring-sleuth.yaml:3)cluster_up_rq_incr.java` | YAML file `spring-sleuth.yaml`, rule index 3 (0-based, the 4th `metricsRules` entry) |
+| LAL | `(default.yaml)default_default.java` | YAML file `default.yaml`, rule named `default_default` |
+| Hierarchy | `(hierarchy-definition.yml)name.java` | Rule `name` in `hierarchy-definition.yml` |
+
+**Notes:**
+- The number after `:` in the MAL source prefix is the 0-based index of the rule within the `metricsRules` list in that YAML file.
+- LAL and Hierarchy rules are standalone entries, so the source prefix contains only the YAML file name without a line number.
+- When source information is unavailable, the SourceFile falls back to just `ClassName.java` without the parenthesized prefix.
+
+### Mapping Back to DSL Source
+
+1. **Identify the DSL type** from the package or class suffix:
+   - `...metrics.generated.*Metrics` or `...Dispatcher` → OAL
+   - `...meter.analyzer.v2.compiler.rt.MalExpr_*` → MAL
+   - `...log.analyzer.v2.compiler.rt.LalExpr_*` → LAL
+   - `...hierarchy.rule.rt.*` → Hierarchy
+
+2. **Find the source file** from the parenthesized prefix (e.g., `core.oal`, `spring-sleuth.yaml`).
+   These files are in the `config/` directory.
+
+3. **Locate the rule** using the line number (OAL) or rule index (MAL) or rule name (LAL/Hierarchy).
+
+4. **Use the statement number** (after the last `:`) as a rough indicator of which operation within the
+   generated method failed. Dump the class (see above) and use `javap -v` to see the exact mapping.
+
+## Common Error Patterns
+
+### OAL Compilation Failure
+
+OAL compilation errors are logged at `ERROR` level during OAP startup:
+
+```
+ERROR o.a.s.o.v.g.OALClassGeneratorV2 - Can't generate method id for ServiceRespTimeMetrics.
+```
+
+This indicates that the generated Java source for the `id` method failed to compile.
+Check the OAL script syntax at the reported metric name.
+
+### MAL/LAL Runtime Error
+
+MAL and LAL errors during metric processing are caught and logged per-expression:
+
+```
+ERROR o.a.s.o.m.a.v.MetricConvert - Analyze Analyzer{...} error
+java.lang.NullPointerException
+    at ...MalExpr_5.run((vm.yaml:2)meter_vm_cpu_total_percentage.java:5)
+```
+
+This tells you: the error is in `vm.yaml`, rule index 2 (the 3rd `metricsRules` entry),
+metric `meter_vm_cpu_total_percentage`, at statement 5 of the generated `run()` method.
+The processing continues for other metrics — a single expression failure does not crash the server.
+
+### Hierarchy Compilation Failure
+
+Hierarchy rule compilation errors are thrown at startup:
+
+```
+IllegalStateException: Failed to compile hierarchy rule: lower-short-name-remove-namespace,
+    expression: { (u, l) -> { if (...) { ... } } }
+```
+
+Check the rule expression syntax in `config/hierarchy-definition.yml` under `auto-matching-rules`.
diff --git a/docs/en/setup/backend/backend-meter.md b/docs/en/setup/backend/backend-meter.md
index 497bee60cc..cf4ee971dc 100644
--- a/docs/en/setup/backend/backend-meter.md
+++ b/docs/en/setup/backend/backend-meter.md
@@ -81,8 +81,6 @@ parameter is optional.
 ### Meters configuration
 
 ```yaml
-# initExp is the expression that initializes the current configuration file
-initExp: <string>
 # filter the metrics, only those metrics that satisfy this condition will be passed into the `metricsRules` below.
 filter: <closure> # example: '{ tags -> tags.job_name == "vm-monitoring" }'
 # expPrefix is executed before the metrics executes other functions.
diff --git a/docs/en/setup/backend/backend-zabbix.md b/docs/en/setup/backend/backend-zabbix.md
index 1ee46a0923..7d84310fa5 100644
--- a/docs/en/setup/backend/backend-zabbix.md
+++ b/docs/en/setup/backend/backend-zabbix.md
@@ -30,8 +30,6 @@ You can find details on Zabbix agent items from [Zabbix Agent documentation](htt
 ### Configuration file
 
 ```yaml
-# initExp is the expression that initializes the current configuration file
-initExp: <string>
 # insert metricPrefix into metric name:  <metricPrefix>_<raw_metric_name>
 metricPrefix: <string>
 # expPrefix is executed before the metrics executes other functions.
diff --git a/docs/en/setup/backend/configuration-vocabulary.md b/docs/en/setup/backend/configuration-vocabulary.md
index c1f512f468..1654a83148 100644
--- a/docs/en/setup/backend/configuration-vocabulary.md
+++ b/docs/en/setup/backend/configuration-vocabulary.md
@@ -549,7 +549,7 @@ process environment and take effect across all modules.
 
 | Environment Variable              | Value(s) and Explanation                                                                                                                                                                                                                                            | Default |
 |-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
-| SW_OAL_ENGINE_DEBUG               | Set to any non-empty value to dump OAL-generated `.class` files to disk (under the `oal-rt/` directory relative to the OAP working path). Useful for debugging code generation issues. Leave unset in production.                                                  | (not set, no files written) |
+| SW_DYNAMIC_CLASS_ENGINE_DEBUG     | Set to any non-empty value to dump dynamically generated `.class` files to disk for all four DSL compilers (OAL, MAL, LAL, Hierarchy). See [Dynamic Code Generation and Debugging](../../operation/dynamic-code-generation-debugging.md) for output directory details. Leave unset in production. | (not set, no files written) |
 | SW_VIRTUAL_THREADS_ENABLED        | Set to `false` to disable virtual threads on JDK 25+. On JDK 25+, gRPC server handler threads and HTTP blocking task executors are virtual threads by default. Set this variable to `false` to force traditional platform thread pools. Ignored on JDK versions below 25. | (not set, virtual threads enabled on JDK 25+) |
 
 ## Note
diff --git a/docs/menu.yml b/docs/menu.yml
index 767d830fb7..4558a252ea 100644
--- a/docs/menu.yml
+++ b/docs/menu.yml
@@ -380,6 +380,10 @@ catalog:
         path: "/en/concepts-and-designs/service-hierarchy-configuration"
       - name: "Metrics Attributes"
         path: "/en/concepts-and-designs/metrics-additional-attributes"
+  - name: "Operation"
+    catalog:
+      - name: "Dynamic Code Generation and Debugging"
+        path: "/en/operation/dynamic-code-generation-debugging"
   - name: "Security Notice"
     path: "/en/security/readme"
   - name: "Academy"
@@ -394,6 +398,8 @@ catalog:
         path: "/en/concepts-and-designs/ebpf-cpu-profiling"
       - name: "Diagnose Service Mesh Network Performance with eBPF"
         path: "/en/academy/diagnose-service-mesh-network-performance-with-ebpf"
+      - name: "DSL Compiler Design"
+        path: "/en/academy/dsl-compiler-design"
   - name: "FAQs"
     path: "/en/faq/readme"
   - name: "Contributing Guides"
@@ -428,6 +434,8 @@ catalog:
                 path: "/en/guides/how-to-bump-up-zipkin"
           - name: "I18n"
             path: "/en/guides/i18n"
+          - name: "Claude Code Skills"
+            path: "/en/guides/claude-code-skills"
   - name: "SWIP"
     path: "/en/swip/readme"
   - name: "Changelog"
diff --git a/oap-server/ai-pipeline/pom.xml b/oap-server/ai-pipeline/pom.xml
index ddcf480d7c..4ec64c5f99 100644
--- a/oap-server/ai-pipeline/pom.xml
+++ b/oap-server/ai-pipeline/pom.xml
@@ -73,8 +73,9 @@
             <scope>test</scope>
         </dependency>
         <dependency>
-            <groupId>org.powermock</groupId>
-            <artifactId>powermock-reflect</artifactId>
+            <groupId>org.apache.skywalking</groupId>
+            <artifactId>server-testing</artifactId>
+            <version>${project.version}</version>
             <scope>test</scope>
         </dependency>
     </dependencies>
diff --git a/oap-server/ai-pipeline/src/test/java/BaselineServerTest.java b/oap-server/ai-pipeline/src/test/java/BaselineServerTest.java
index 2685a849af..57021d818a 100644
--- a/oap-server/ai-pipeline/src/test/java/BaselineServerTest.java
+++ b/oap-server/ai-pipeline/src/test/java/BaselineServerTest.java
@@ -30,7 +30,7 @@ import org.apache.skywalking.oap.server.core.analysis.TimeBucket;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.powermock.reflect.Whitebox;
+import org.apache.skywalking.oap.server.testing.util.ReflectUtil;
 
 import java.io.IOException;
 import java.util.Arrays;
@@ -63,7 +63,7 @@ public class BaselineServerTest {
 
         queryService = new BaselineQueryServiceImpl("", 0);
         org.apache.skywalking.apm.baseline.v3.AlarmBaselineServiceGrpc.AlarmBaselineServiceBlockingStub blockingStub = org.apache.skywalking.apm.baseline.v3.AlarmBaselineServiceGrpc.newBlockingStub(channel);
-        Whitebox.setInternalState(queryService, "stub", blockingStub);
+        ReflectUtil.setInternalState(queryService, "stub", blockingStub);
     }
 
     @AfterEach
diff --git a/oap-server/analyzer/agent-analyzer/pom.xml b/oap-server/analyzer/agent-analyzer/pom.xml
index b4b96cd62e..5a28180058 100644
--- a/oap-server/analyzer/agent-analyzer/pom.xml
+++ b/oap-server/analyzer/agent-analyzer/pom.xml
@@ -43,5 +43,11 @@
             <artifactId>meter-analyzer</artifactId>
             <version>${project.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.skywalking</groupId>
+            <artifactId>server-testing</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>
\ No newline at end of file
diff --git a/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/meter/config/MeterConfig.java b/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/meter/config/MeterConfig.java
index 4bc3bec8c7..8e13ab8c2c 100644
--- a/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/meter/config/MeterConfig.java
+++ b/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/meter/config/MeterConfig.java
@@ -20,7 +20,7 @@ package org.apache.skywalking.oap.server.analyzer.provider.meter.config;
 
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.apache.skywalking.oap.meter.analyzer.MetricRuleConfig;
+import org.apache.skywalking.oap.meter.analyzer.v2.MetricRuleConfig;
 
 import java.util.List;
 
@@ -32,7 +32,6 @@ public class MeterConfig implements MetricRuleConfig {
     private String expPrefix;
     private String filter;
     private List<Rule> metricsRules;
-    private String initExp;
 
     @Data
     @NoArgsConstructor
diff --git a/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/meter/process/MeterProcessService.java b/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/meter/process/MeterProcessService.java
index e3f30c9680..94d0909e5b 100644
--- a/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/meter/process/MeterProcessService.java
+++ b/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/meter/process/MeterProcessService.java
@@ -18,7 +18,7 @@
 
 package org.apache.skywalking.oap.server.analyzer.provider.meter.process;
 
-import org.apache.skywalking.oap.meter.analyzer.MetricConvert;
+import org.apache.skywalking.oap.meter.analyzer.v2.MetricConvert;
 import org.apache.skywalking.oap.server.analyzer.provider.meter.config.MeterConfig;
 import org.apache.skywalking.oap.server.core.CoreModule;
 import org.apache.skywalking.oap.server.core.analysis.meter.MeterSystem;
diff --git a/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/meter/process/MeterProcessor.java b/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/meter/process/MeterProcessor.java
index d94afe0cdf..4635c5084e 100644
--- a/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/meter/process/MeterProcessor.java
+++ b/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/meter/process/MeterProcessor.java
@@ -27,9 +27,9 @@ import org.apache.skywalking.apm.network.language.agent.v3.MeterData;
 import org.apache.skywalking.apm.network.language.agent.v3.MeterHistogram;
 import org.apache.skywalking.apm.network.language.agent.v3.MeterSingleValue;
 import org.apache.skywalking.oap.server.library.util.StringUtil;
-import org.apache.skywalking.oap.meter.analyzer.MetricConvert;
-import org.apache.skywalking.oap.meter.analyzer.dsl.Sample;
-import org.apache.skywalking.oap.meter.analyzer.dsl.SampleFamilyBuilder;
+import org.apache.skywalking.oap.meter.analyzer.v2.MetricConvert;
+import org.apache.skywalking.oap.meter.analyzer.v2.dsl.Sample;
+import org.apache.skywalking.oap.meter.analyzer.v2.dsl.SampleFamilyBuilder;
 import org.apache.skywalking.oap.server.library.util.CollectionUtils;
 
 import java.util.ArrayList;
@@ -53,7 +53,7 @@ public class MeterProcessor {
     private final MeterProcessService processService;
 
     /**
-     * All of meters has been read. Using it to process groovy script.
+     * All of meters has been read. Using it to process MAL expressions.
      */
     private final Map<String, List<SampleBuilder>> meters = new HashMap<>();
 
diff --git a/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/meter/process/SampleBuilder.java b/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/meter/process/SampleBuilder.java
index ed656e24f3..e7afa79b0a 100644
--- a/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/meter/process/SampleBuilder.java
+++ b/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/meter/process/SampleBuilder.java
@@ -20,7 +20,7 @@ package org.apache.skywalking.oap.server.analyzer.provider.meter.process;
 
 import com.google.common.collect.ImmutableMap;
 import lombok.Builder;
-import org.apache.skywalking.oap.meter.analyzer.dsl.Sample;
+import org.apache.skywalking.oap.meter.analyzer.v2.dsl.Sample;
 
 /**
  * Help to build Sample with agent side meter.
diff --git a/oap-server/analyzer/agent-analyzer/src/test/java/org/apache/skywalking/oap/server/analyzer/provider/meter/process/MeterProcessorTest.java b/oap-server/analyzer/agent-analyzer/src/test/java/org/apache/skywalking/oap/server/analyzer/provider/meter/process/MeterProcessorTest.java
index 6898f4e453..8d9e94f739 100644
--- a/oap-server/analyzer/agent-analyzer/src/test/java/org/apache/skywalking/oap/server/analyzer/provider/meter/process/MeterProcessorTest.java
+++ b/oap-server/analyzer/agent-analyzer/src/test/java/org/apache/skywalking/oap/server/analyzer/provider/meter/process/MeterProcessorTest.java
@@ -49,7 +49,7 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
-import org.powermock.reflect.Whitebox;
+import org.apache.skywalking.oap.server.testing.util.ReflectUtil;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
@@ -83,7 +83,7 @@ public class MeterProcessorTest {
         when(moduleManager.find(CoreModule.NAME).provider()).thenReturn(mock(ModuleServiceHolder.class));
         when(moduleManager.find(CoreModule.NAME).provider().getService(MeterSystem.class)).thenReturn(meterSystem);
         MetricsStreamProcessor mockProcessor = mock(MetricsStreamProcessor.class);
-        Whitebox.setInternalState(
+        ReflectUtil.setInternalState(
                 MetricsStreamProcessor.class,
                 "PROCESSOR",
                 mockProcessor
diff --git a/oap-server/analyzer/agent-analyzer/src/test/java/org/apache/skywalking/oap/server/analyzer/provider/trace/TraceSamplingPolicyWatcherTest.java b/oap-server/analyzer/agent-analyzer/src/test/java/org/apache/skywalking/oap/server/analyzer/provider/trace/TraceSamplingPolicyWatcherTest.java
index 445aa6e1a2..25e8c45631 100644
--- a/oap-server/analyzer/agent-analyzer/src/test/java/org/apache/skywalking/oap/server/analyzer/provider/trace/TraceSamplingPolicyWatcherTest.java
+++ b/oap-server/analyzer/agent-analyzer/src/test/java/org/apache/skywalking/oap/server/analyzer/provider/trace/TraceSamplingPolicyWatcherTest.java
@@ -33,7 +33,7 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Timeout;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.junit.jupiter.MockitoExtension;
-import org.powermock.reflect.Whitebox;
+import org.apache.skywalking.oap.server.testing.util.ReflectUtil;
 
 import java.util.Optional;
 import java.util.Set;
@@ -232,7 +232,7 @@ public class TraceSamplingPolicyWatcherTest {
         ConfigWatcherRegister register = new ServiceMockConfigWatcherRegister(3);
 
         TraceSamplingPolicyWatcher watcher = new TraceSamplingPolicyWatcher(moduleConfig, provider);
-        Whitebox.setInternalState(provider, "moduleConfig", moduleConfig);
+        ReflectUtil.setInternalState(provider, "moduleConfig", moduleConfig);
         provider.getModuleConfig().setTraceSamplingPolicyWatcher(watcher);
         register.registerConfigChangeWatcher(watcher);
         register.start();
@@ -369,7 +369,7 @@ public class TraceSamplingPolicyWatcherTest {
     }
 
     private SamplingPolicy getSamplingPolicy(String service, TraceSamplingPolicyWatcher watcher) {
-        AtomicReference<SamplingPolicySettings> samplingPolicySettings = Whitebox.getInternalState(
+        AtomicReference<SamplingPolicySettings> samplingPolicySettings = ReflectUtil.getInternalState(
             watcher, "samplingPolicySettings");
         return samplingPolicySettings.get().get(service);
     }
diff --git a/oap-server/analyzer/agent-analyzer/src/test/java/org/apache/skywalking/oap/server/analyzer/provider/trace/UninstrumentedGatewaysConfigTest.java b/oap-server/analyzer/agent-analyzer/src/test/java/org/apache/skywalking/oap/server/analyzer/provider/trace/UninstrumentedGatewaysConfigTest.java
index 1824fa0b76..f64467e2ac 100644
--- a/oap-server/analyzer/agent-analyzer/src/test/java/org/apache/skywalking/oap/server/analyzer/provider/trace/UninstrumentedGatewaysConfigTest.java
+++ b/oap-server/analyzer/agent-analyzer/src/test/java/org/apache/skywalking/oap/server/analyzer/provider/trace/UninstrumentedGatewaysConfigTest.java
@@ -24,7 +24,7 @@ import org.apache.skywalking.oap.server.library.module.ModuleStartException;
 import org.apache.skywalking.oap.server.library.module.ServiceNotProvidedException;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
-import org.powermock.reflect.Whitebox;
+import org.apache.skywalking.oap.server.testing.util.ReflectUtil;
 
 public class UninstrumentedGatewaysConfigTest {
     @Test
@@ -32,7 +32,7 @@ public class UninstrumentedGatewaysConfigTest {
         final UninstrumentedGatewaysConfig uninstrumentedGatewaysConfig
             = new UninstrumentedGatewaysConfig(new MockProvider());
         UninstrumentedGatewaysConfig.GatewayInfos gatewayInfos
-            = Whitebox.invokeMethod(uninstrumentedGatewaysConfig, "parseGatewaysFromFile", "gateways.yml");
+            = ReflectUtil.invokeMethod(uninstrumentedGatewaysConfig, "parseGatewaysFromFile", "gateways.yml");
         Assertions.assertEquals(1, gatewayInfos.getGateways().size());
     }
 
diff --git a/oap-server/analyzer/hierarchy/CLAUDE.md b/oap-server/analyzer/hierarchy/CLAUDE.md
new file mode 100644
index 0000000000..76569131cc
--- /dev/null
+++ b/oap-server/analyzer/hierarchy/CLAUDE.md
@@ -0,0 +1,140 @@
+# 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
+    CompiledHierarchyRuleProvider.java          — SPI provider: compiles rule expressions
+    hierarchy/rule/rt/
+      HierarchyRulePackageHolder.java           — Class loading anchor (empty marker)
+
+  src/main/resources/META-INF/services/
+    ...HierarchyDefinitionService$HierarchyRuleProvider — SPI registration
+
+  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.v2.compiler` |
+| Generated classes | `org.apache.skywalking.oap.server.core.config.v2.compiler.hierarchy.rule.rt.HierarchyRule_<N>` |
+| Package holder | `org.apache.skywalking.oap.server.core.config.v2.compiler.hierarchy.rule.rt.HierarchyRulePackageHolder` |
+| SPI provider | `org.apache.skywalking.oap.server.core.config.v2.compiler.CompiledHierarchyRuleProvider` |
+| 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) }` |
+
+## Hierarchy Input Data Mock Principles
+
+Hierarchy rules are simpler than MAL/LAL — they take two `Service` objects and return a boolean. The test data is implicit in the test code rather than YAML files.
+
+### Principles
+
+1. **Service objects are the input**: Each rule receives `(Service upper, Service lower)` and returns whether they match. Test data is built programmatically with `Service.builder().name("...").shortName("...").build()`.
+2. **Four rule patterns**: See "Rule Patterns" above. Tests cover all four with various `name`/`shortName` combinations.
+3. **v1-v2 comparison**: The checker test (`HierarchyComparisonTest` in `mal-lal-v1-v2-checker`) compiles each rule with both v1 (Groovy) and v2 (ANTLR4), runs them on the same `Service` pairs, and asserts identical results.
+4. **No `.data.yaml` files**: Hierarchy rules are purely functional (two inputs → boolean), so mock data is inline in tests.
+
+## Debug Output
+
+When `SW_DYNAMIC_CLASS_ENGINE_DEBUG=true` environment variable is set, generated `.class` files are written to disk for inspection:
+
+```
+{skywalking}/hierarchy-rt/
+  *.class          - Generated HierarchyRule .class files
+```
+
+This is the same env variable used by OAL. Useful for debugging code generation issues. In tests, use `setClassOutputDir(dir)` instead.
+
+## 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/meter-analyzer/pom.xml b/oap-server/analyzer/hierarchy/pom.xml
similarity index 66%
copy from oap-server/analyzer/meter-analyzer/pom.xml
copy to oap-server/analyzer/hierarchy/pom.xml
index 945fd061b3..04adce9ed0 100644
--- a/oap-server/analyzer/meter-analyzer/pom.xml
+++ b/oap-server/analyzer/hierarchy/pom.xml
@@ -25,26 +25,41 @@
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
-    <artifactId>meter-analyzer</artifactId>
+    <artifactId>hierarchy</artifactId>
 
     <dependencies>
-        <dependency>
-            <groupId>org.apache.skywalking</groupId>
-            <artifactId>library-kubernetes-support</artifactId>
-            <version>${project.version}</version>
-        </dependency>
         <dependency>
             <groupId>org.apache.skywalking</groupId>
             <artifactId>server-core</artifactId>
             <version>${project.version}</version>
         </dependency>
         <dependency>
-            <groupId>org.apache.groovy</groupId>
-            <artifactId>groovy</artifactId>
+            <groupId>org.antlr</groupId>
+            <artifactId>antlr4-runtime</artifactId>
         </dependency>
         <dependency>
-            <groupId>io.vavr</groupId>
-            <artifactId>vavr</artifactId>
+            <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/v2/compiler/CompiledHierarchyRuleProvider.java b/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/v2/compiler/CompiledHierarchyRuleProvider.java
new file mode 100644
index 0000000000..f1607d2369
--- /dev/null
+++ b/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/v2/compiler/CompiledHierarchyRuleProvider.java
@@ -0,0 +1,73 @@
+/*
+ * 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.v2.compiler;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiFunction;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.skywalking.oap.server.core.config.HierarchyDefinitionService;
+import org.apache.skywalking.oap.server.core.query.type.Service;
+
+/**
+ * SPI implementation of {@link HierarchyDefinitionService.HierarchyRuleProvider}
+ * that compiles hierarchy matching rule expressions using ANTLR4 + Javassist.
+ *
+ * <p>Discovered at startup via {@code ServiceLoader} by
+ * {@link HierarchyDefinitionService}. For each rule expression
+ * (e.g., {@code "{ (u, l) -> u.name == l.name }"}):
+ * <ol>
+ *   <li>{@link HierarchyRuleClassGenerator#compile} parses the expression
+ *       with ANTLR4 into an AST, then generates a Java class implementing
+ *       {@code BiFunction<Service, Service, Boolean>} via Javassist.</li>
+ *   <li>The generated class casts both arguments to {@link Service},
+ *       evaluates the expression body, and returns a {@code Boolean}.</li>
+ * </ol>
+ *
+ * <p>The compiled matchers are returned to {@link HierarchyDefinitionService}
+ * and used at runtime by
+ * {@link org.apache.skywalking.oap.server.core.hierarchy.HierarchyService}
+ * to match service pairs.
+ */
+@Slf4j
+public class CompiledHierarchyRuleProvider implements HierarchyDefinitionService.HierarchyRuleProvider {
+
+    private final HierarchyRuleClassGenerator generator;
+
+    public CompiledHierarchyRuleProvider() {
+        generator = new HierarchyRuleClassGenerator();
+        generator.setYamlSource("hierarchy-definition.yml");
+    }
+
+    @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) -> {
+            try {
+                rules.put(name, generator.compile(name, expression));
+                log.debug("Compiled hierarchy rule: {}", name);
+            } catch (Exception e) {
+                throw new IllegalStateException(
+                    "Failed to compile hierarchy rule: " + name
+                        + ", expression: " + expression, e);
+            }
+        });
+        return rules;
+    }
+}
diff --git a/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/v2/compiler/HierarchyRuleClassGenerator.java b/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/v2/compiler/HierarchyRuleClassGenerator.java
new file mode 100644
index 0000000000..62e6e37a43
--- /dev/null
+++ b/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/v2/compiler/HierarchyRuleClassGenerator.java
@@ -0,0 +1,543 @@
+/*
+ * 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.v2.compiler;
+
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+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 lombok.extern.slf4j.Slf4j;
+import org.apache.skywalking.oap.server.core.WorkPath;
+import org.apache.skywalking.oap.server.core.config.v2.compiler.hierarchy.rule.rt.HierarchyRulePackageHolder;
+import org.apache.skywalking.oap.server.library.util.StringUtil;
+import org.apache.skywalking.oap.server.core.query.type.Service;
+
+/**
+ * Generates {@link BiFunction BiFunction&lt;Service, Service, Boolean&gt;} implementation classes
+ * from {@link HierarchyRuleModel} AST using Javassist bytecode generation.
+ *
+ * <p>Rule expressions are parsed from {@code hierarchy-definition.yml} {@code auto-matching-rules}
+ * section. The grammar ({@code HierarchyRuleParser.g4}) supports:
+ * <ul>
+ *   <li>Property access on {@link Service}: {@code u.name}, {@code l.shortName}
+ *       — mapped to getter methods ({@code getName()}, {@code getShortName()})
+ *       via {@link #toGetter(String)}</li>
+ *   <li>String method calls: {@code substring()}, {@code lastIndexOf()}, {@code concat()}</li>
+ *   <li>Comparisons: {@code ==}, {@code !=} (via {@code Objects.equals()}),
+ *       {@code >}, {@code <}, {@code >=}, {@code <=}</li>
+ *   <li>Logical operators: {@code &&}, {@code ||}, {@code !}</li>
+ *   <li>Control flow: {@code if/else}, {@code return}</li>
+ *   <li>Literals: strings, numbers, booleans</li>
+ * </ul>
+ *
+ * <p>Debugging support in generated bytecode:
+ * <ul>
+ *   <li><b>SourceFile</b>: set via {@link #formatSourceFileName(String)} to
+ *       {@code (hierarchy-definition.yml)ruleName.java} so stack traces show the
+ *       originating YAML file and rule name</li>
+ *   <li><b>LineNumberTable</b>: added via {@link #addLineNumberTable} so debuggers
+ *       can step through generated code</li>
+ *   <li><b>LocalVariableTable</b>: added via {@link #addLocalVariableTable} with
+ *       the original parameter names (e.g., {@code u}, {@code l}) for debugger
+ *       variable inspection</li>
+ * </ul>
+ */
+@Slf4j
+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.v2.compiler.hierarchy.rule.rt.";
+
+    private static final java.util.Set<String> USED_CLASS_NAMES =
+        java.util.Collections.synchronizedSet(new java.util.HashSet<>());
+
+    private final ClassPool classPool;
+    private File classOutputDir;
+    private String classNameHint;
+    private String yamlSource;
+
+    public HierarchyRuleClassGenerator() {
+        this(ClassPool.getDefault());
+        if (StringUtil.isNotEmpty(System.getenv("SW_DYNAMIC_CLASS_ENGINE_DEBUG"))) {
+            classOutputDir = new File(WorkPath.getPath().getParentFile(), "hierarchy-rt");
+        }
+    }
+
+    public HierarchyRuleClassGenerator(final ClassPool classPool) {
+        this.classPool = classPool;
+    }
+
+    public void setClassOutputDir(final File dir) {
+        this.classOutputDir = dir;
+    }
+
+    public void setClassNameHint(final String hint) {
+        this.classNameHint = hint;
+    }
+
+    public void setYamlSource(final String yamlSource) {
+        this.yamlSource = yamlSource;
+    }
+
+    private String makeClassName(final String defaultPrefix) {
+        if (classNameHint != null) {
+            return dedupClassName(PACKAGE_PREFIX + sanitizeName(classNameHint));
+        }
+        return PACKAGE_PREFIX + defaultPrefix + CLASS_COUNTER.getAndIncrement();
+    }
+
+    private String dedupClassName(final String base) {
+        if (USED_CLASS_NAMES.add(base)) {
+            return base;
+        }
+        for (int i = 2; ; i++) {
+            final String candidate = base + "_" + i;
+            if (USED_CLASS_NAMES.add(candidate)) {
+                return candidate;
+            }
+        }
+    }
+
+    private static String sanitizeName(final String name) {
+        final StringBuilder sb = new StringBuilder(name.length());
+        for (int i = 0; i < name.length(); i++) {
+            final char c = name.charAt(i);
+            sb.append(i == 0
+                ? (Character.isJavaIdentifierStart(c) ? c : '_')
+                : (Character.isJavaIdentifierPart(c) ? c : '_'));
+        }
+        return sb.length() == 0 ? "Generated" : sb.toString();
+    }
+
+    /**
+     * Builds the {@code SourceFile} attribute value embedded in the generated class bytecode.
+     * Format: {@code (hierarchy-definition.yml)name.java} so that stack traces show both
+     * the originating YAML file and the rule name. Falls back to just {@code name.java}
+     * when {@code yamlSource} is not set (e.g., in unit tests).
+     */
+    private String formatSourceFileName(final String ruleName) {
+        final String classFile = sanitizeName(ruleName) + ".java";
+        if (yamlSource != null) {
+            return "(" + yamlSource + ")" + classFile;
+        }
+        return classFile;
+    }
+
+    private static void setSourceFile(final CtClass ctClass, final String name) {
+        try {
+            final javassist.bytecode.ClassFile cf = ctClass.getClassFile();
+            final javassist.bytecode.AttributeInfo sf = cf.getAttribute("SourceFile");
+            if (sf != null) {
+                final javassist.bytecode.ConstPool cp = cf.getConstPool();
+                final int idx = cp.addUtf8Info(name);
+                sf.set(new byte[]{(byte) (idx >> 8), (byte) idx});
+            }
+        } catch (Exception e) {
+            // best-effort
+        }
+    }
+
+    private void addLineNumberTable(final javassist.CtMethod method,
+                                     final int firstResultSlot) {
+        try {
+            final javassist.bytecode.MethodInfo mi = method.getMethodInfo();
+            final javassist.bytecode.CodeAttribute code = mi.getCodeAttribute();
+            if (code == null) {
+                return;
+            }
+
+            final java.util.ArrayList<int[]> entries = new java.util.ArrayList<>();
+            int line = 1;
+            boolean nextIsNewLine = true;
+
+            final javassist.bytecode.CodeIterator ci = code.iterator();
+            while (ci.hasNext()) {
+                final int pc = ci.next();
+                if (nextIsNewLine) {
+                    entries.add(new int[]{pc, line++});
+                    nextIsNewLine = false;
+                }
+                final int op = ci.byteAt(pc) & 0xFF;
+                int slot = -1;
+                if (op >= 59 && op <= 78) {
+                    slot = (op - 59) % 4;
+                } else if (op >= 54 && op <= 58) {
+                    slot = ci.byteAt(pc + 1) & 0xFF;
+                }
+                if (slot >= firstResultSlot) {
+                    nextIsNewLine = true;
+                }
+            }
+
+            if (entries.isEmpty()) {
+                return;
+            }
+
+            final javassist.bytecode.ConstPool cp = mi.getConstPool();
+            final byte[] info = new byte[2 + entries.size() * 4];
+            info[0] = (byte) (entries.size() >> 8);
+            info[1] = (byte) entries.size();
+            for (int i = 0; i < entries.size(); i++) {
+                final int off = 2 + i * 4;
+                info[off] = (byte) (entries.get(i)[0] >> 8);
+                info[off + 1] = (byte) entries.get(i)[0];
+                info[off + 2] = (byte) (entries.get(i)[1] >> 8);
+                info[off + 3] = (byte) entries.get(i)[1];
+            }
+            code.getAttributes().add(
+                new javassist.bytecode.AttributeInfo(cp, "LineNumberTable", info));
+        } catch (Exception e) {
+            log.warn("Failed to add LineNumberTable: {}", e.getMessage());
+        }
+    }
+
+    private void writeClassFile(final CtClass ctClass) {
+        if (classOutputDir == null) {
+            return;
+        }
+        if (!classOutputDir.exists()) {
+            classOutputDir.mkdirs();
+        }
+        final File file = new File(classOutputDir, ctClass.getSimpleName() + ".class");
+        try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
+            ctClass.toBytecode(out);
+        } catch (Exception e) {
+            log.warn("Failed to write class file {}: {}", file, e.getMessage());
+        }
+    }
+
+    private void addLocalVariableTable(final javassist.CtMethod method,
+                                       final String className,
+                                       final String[][] vars) {
+        try {
+            final javassist.bytecode.MethodInfo mi = method.getMethodInfo();
+            final javassist.bytecode.CodeAttribute code = mi.getCodeAttribute();
+            if (code == null) {
+                return;
+            }
+            final javassist.bytecode.ConstPool cp = mi.getConstPool();
+            final int len = code.getCodeLength();
+            final javassist.bytecode.LocalVariableAttribute lva =
+                new javassist.bytecode.LocalVariableAttribute(cp);
+            lva.addEntry(0, len,
+                cp.addUtf8Info("this"),
+                cp.addUtf8Info("L" + className.replace('.', '/') + ";"), 0);
+            for (int i = 0; i < vars.length; i++) {
+                lva.addEntry(0, len,
+                    cp.addUtf8Info(vars[i][0]),
+                    cp.addUtf8Info(vars[i][1]), i + 1);
+            }
+            code.getAttributes().add(lva);
+        } catch (Exception e) {
+            log.warn("Failed to add LocalVariableTable: {}", e.getMessage());
+        }
+    }
+
+    /**
+     * Compiles a hierarchy rule expression into a {@code BiFunction} class.
+     *
+     * <p>Flow: expression → {@link HierarchyRuleScriptParser#parse} (ANTLR4 AST)
+     * → {@link #generateApplyMethod} (Java source) → Javassist compile → class load.
+     * Property access (e.g., {@code u.name}) is mapped to getter calls
+     * (e.g., {@code u.getName()}) via {@link #toGetter(String)}.
+     * Equality comparisons use {@code java.util.Objects.equals()} for null safety.
+     *
+     * <p>If parsing or compilation fails, throws immediately (fail-fast at startup).
+     *
+     * @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 saved = classNameHint;
+        if (classNameHint == null) {
+            classNameHint = ruleName;
+        }
+        final String className;
+        try {
+            className = makeClassName("HierarchyRule_");
+        } finally {
+            classNameHint = saved;
+        }
+
+        final CtClass ctClass = classPool.makeClass(className);
+        ctClass.addInterface(classPool.get("java.util.function.BiFunction"));
+
+        ctClass.addConstructor(CtNewConstructor.defaultConstructor(ctClass));
+
+        final String applyBody = generateApplyMethod(model);
+
+        if (log.isDebugEnabled()) {
+            log.debug("Hierarchy compile [{}] AST: {}", ruleName, model);
+            log.debug("Hierarchy compile [{}] apply():\n{}", ruleName, applyBody);
+        }
+
+        final javassist.CtMethod applyMethod = CtNewMethod.make(applyBody, ctClass);
+        ctClass.addMethod(applyMethod);
+        addLineNumberTable(applyMethod, 3);
+        final String svcDesc = "Lorg/apache/skywalking/oap/server/core/query/type/Service;";
+        addLocalVariableTable(applyMethod, className, new String[][]{
+            {"arg0", "Ljava/lang/Object;"},
+            {"arg1", "Ljava/lang/Object;"},
+            {model.getUpperParam(), svcDesc},
+            {model.getLowerParam(), svcDesc}
+        });
+
+        setSourceFile(ctClass, formatSourceFileName(ruleName));
+        writeClassFile(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(')');
+            }
+        }
+    }
+
+    /**
+     * Maps a field name in rule expressions to the corresponding getter method
+     * on {@link Service}. Known fields ({@code name} → {@code getName},
+     * {@code shortName} → {@code getShortName}) are hard-coded for clarity;
+     * unknown fields fall back to JavaBean convention ({@code foo} → {@code getFoo}).
+     * If the getter does not exist on {@link Service}, Javassist compilation will
+     * fail at startup with a clear error.
+     */
+    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/v2/compiler/HierarchyRuleModel.java b/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/v2/compiler/HierarchyRuleModel.java
new file mode 100644
index 0000000000..b0f587871b
--- /dev/null
+++ b/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/v2/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.v2.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) -&gt; u.name == l.name }
+ *   { (u, l) -&gt; { if(l.shortName.lastIndexOf('.') &gt; 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/v2/compiler/HierarchyRuleScriptParser.java b/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/v2/compiler/HierarchyRuleScriptParser.java
new file mode 100644
index 0000000000..b409372c4c
--- /dev/null
+++ b/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/v2/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.v2.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) -&gt; 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/DownsamplingType.java b/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/v2/compiler/hierarchy/rule/rt/HierarchyRulePackageHolder.java
similarity index 72%
copy from oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/DownsamplingType.java
copy to oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/v2/compiler/hierarchy/rule/rt/HierarchyRulePackageHolder.java
index c8f0006fbb..6ad8dd96b8 100644
--- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/DownsamplingType.java
+++ b/oap-server/analyzer/hierarchy/src/main/java/org/apache/skywalking/oap/server/core/config/v2/compiler/hierarchy/rule/rt/HierarchyRulePackageHolder.java
@@ -13,14 +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;
+package org.apache.skywalking.oap.server.core.config.v2.compiler.hierarchy.rule.rt;
 
 /**
- * DownsamplingType indicates the downsampling type of meter function
+ * 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.
  */
-public enum DownsamplingType {
-    AVG, SUM, LATEST, SUM_PER_MIN, MAX, MIN
+public class HierarchyRulePackageHolder {
 }
diff --git a/oap-server/analyzer/log-analyzer/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.library.module.ModuleDefine b/oap-server/analyzer/hierarchy/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.core.config.HierarchyDefinitionService$HierarchyRuleProvider
similarity index 90%
copy from oap-server/analyzer/log-analyzer/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.library.module.ModuleDefine
copy to oap-server/analyzer/hierarchy/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.core.config.HierarchyDefinitionService$HierarchyRuleProvider
index 54d5a91d08..66092b3355 100644
--- a/oap-server/analyzer/log-analyzer/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.library.module.ModuleDefine
+++ b/oap-server/analyzer/hierarchy/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.core.config.HierarchyDefinitionService$HierarchyRuleProvider
@@ -16,4 +16,4 @@
 #
 #
 
-org.apache.skywalking.oap.log.analyzer.module.LogAnalyzerModule
\ No newline at end of file
+org.apache.skywalking.oap.server.core.config.v2.compiler.CompiledHierarchyRuleProvider
diff --git a/oap-server/analyzer/hierarchy/src/test/java/org/apache/skywalking/oap/server/core/config/v2/compiler/HierarchyRuleClassGeneratorTest.java b/oap-server/analyzer/hierarchy/src/test/java/org/apache/skywalking/oap/server/core/config/v2/compiler/HierarchyRuleClassGeneratorTest.java
new file mode 100644
index 0000000000..72488ef61c
--- /dev/null
+++ b/oap-server/analyzer/hierarchy/src/test/java/org/apache/skywalking/oap/server/core/config/v2/compiler/HierarchyRuleClassGeneratorTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.v2.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.assertThrows;
+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));
+    }
+
+    // ==================== Error handling tests ====================
+
+    @Test
+    void emptyExpressionThrows() {
+        // Demo error: Hierarchy rule parsing failed: 1:0 mismatched input '<EOF>'
+        //   expecting '{'
+        assertThrows(Exception.class,
+            () -> generator.compile("empty", ""));
+    }
+
+    @Test
+    void missingClosureBracesThrows() {
+        // Demo error: Hierarchy rule parsing failed: 1:0 mismatched input 'u'
+        //   expecting '{'
+        assertThrows(Exception.class,
+            () -> generator.compile("test", "u.name == l.name"));
+    }
+
+    @Test
+    void missingParametersThrows() {
+        // Demo error: Hierarchy rule parsing failed: 1:2 mismatched input '}'
+        //   expecting '('
+        assertThrows(Exception.class,
+            () -> generator.compile("test", "{ }"));
+    }
+
+    @Test
+    void invalidFieldAccessThrows() {
+        // Demo error: [source error] getNonExistent() not found in Service
+        // (Javassist cannot find the getter for a non-existent field)
+        assertThrows(Exception.class,
+            () -> generator.compile("test",
+                "{ (u, l) -> u.nonExistent == l.nonExistent }"));
+    }
+}
diff --git a/oap-server/analyzer/hierarchy/src/test/java/org/apache/skywalking/oap/server/core/config/v2/compiler/HierarchyRuleScriptParserTest.java b/oap-server/analyzer/hierarchy/src/test/java/org/apache/skywalking/oap/server/core/config/v2/compiler/HierarchyRuleScriptParserTest.java
new file mode 100644
index 0000000000..9fa60481bf
--- /dev/null
+++ b/oap-server/analyzer/hierarchy/src/test/java/org/apache/skywalking/oap/server/core/config/v2/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.v2.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/log-analyzer/CLAUDE.md b/oap-server/analyzer/log-analyzer/CLAUDE.md
new file mode 100644
index 0000000000..881a40ac7a
--- /dev/null
+++ b/oap-server/analyzer/log-analyzer/CLAUDE.md
@@ -0,0 +1,263 @@
+# 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)
+      1. detectParserType(model)     — compile-time data source analysis (JSON/YAML/TEXT/NONE)
+      2. generateExecuteMethod()     — emit execute() + private methods (_extractor, _sink)
+      3. classPool.makeClass()       — single class implementing LalExpression
+      4. addLocalVariableTable()     — named LVT entries for all methods
+      5. ctClass.toClass()           — load into JVM
+  → LalExpression instance
+```
+
+The generated class implements:
+```java
+void execute(FilterSpec filterSpec, ExecutionContext ctx)
+```
+
+## 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              — Public API, execute method codegen, class scaffolding
+    LALBlockCodegen.java                — Extractor/sink/condition/value-access codegen
+    LALCodegenHelper.java               — Static utility methods and shared constants
+    rt/
+      LalExpressionPackageHolder.java   — Class loading anchor (empty marker)
+      LalRuntimeHelper.java             — Instance-based helper called by generated code
+
+  src/main/java/.../dsl/
+    LalExpression.java                  — Functional interface: execute(FilterSpec, ExecutionContext)
+    ExecutionContext.java               — Per-log execution state (log, parsed, flags)
+    DSL.java                            — Wraps compiled expression + FilterSpec
+    spec/filter/FilterSpec.java         — Top-level filter spec (all methods take ctx explicitly)
+    spec/extractor/ExtractorSpec.java   — Extractor field setters (all methods take ctx explicitly)
+    spec/sink/SinkSpec.java             — Sink spec (save/drop/sample)
+    spec/sink/SamplerSpec.java          — Rate-limit sampler
+
+  src/test/java/.../compiler/
+    LALScriptParserTest.java            — 20 parser tests
+    LALClassGeneratorTest.java          — 37 generator tests
+    LALExpressionExecutionTest.java     — 27 data-driven execution tests (from YAML + .input.data)
+```
+
+## Package & Class Naming
+
+All v2 classes live under `org.apache.skywalking.oap.log.analyzer.v2.*` to avoid FQCN conflicts with the v1 (Groovy) classes.
+
+| Component | Package / Name |
+|-----------|---------------|
+| Parser/Model/Generator | `org.apache.skywalking.oap.log.analyzer.v2.compiler` |
+| Generated classes | `org.apache.skywalking.oap.log.analyzer.v2.compiler.rt.LalExpr_<N>` |
+| Package holder | `org.apache.skywalking.oap.log.analyzer.v2.compiler.rt.LalExpressionPackageHolder` |
+| Runtime helper | `org.apache.skywalking.oap.log.analyzer.v2.compiler.rt.LalRuntimeHelper` |
+| Functional interface | `org.apache.skywalking.oap.log.analyzer.v2.dsl.LalExpression` |
+
+`<N>` is a global `AtomicInteger` counter.
+
+## Single Class with Private Methods
+
+The generator produces a single class per LAL script. Extractor and sink blocks become private methods called directly from `execute()` — no Consumer classes, no callback indirection.
+
+Method naming: `_extractor`, `_extractor_2`, `_extractor_3` (no `_0` suffix for single methods).
+
+Sub-blocks (slowSql, sampledTrace, metrics, sampler, rateLimit) are inlined within their parent method.
+
+## Explicit Context Passing (No ThreadLocal)
+
+All spec methods take `ExecutionContext ctx` as an explicit parameter — there is no `BINDING` ThreadLocal or `bind()` method. The `execute()` method receives `ctx` directly and passes it through:
+
+- `execute(FilterSpec filterSpec, ExecutionContext ctx)` — entry point
+- `filterSpec.json(ctx)`, `filterSpec.text(ctx)`, `filterSpec.sink(ctx)` — parser/sink calls
+- `_e.service(h.ctx(), ...)`, `_e.tag(h.ctx(), ...)` — extractor calls via `h.ctx()`
+- `_f.sampler().rateLimit(h.ctx(), ...)` — sink calls via `h.ctx()`
+
+The generated `execute()` method guards `_extractor()` and `_sink()` calls with `if (!ctx.shouldAbort())`, matching v1 Groovy behavior where `extractor {}` and `sink {}` closures check the abort flag before running their body. `finalizeSink(ctx)` also checks the flag. Individual spec methods inside each block additionally check `ctx.shouldAbort()` as a defense-in-depth measure.
+
+## LocalVariableTable (LVT)
+
+All generated methods include a `LocalVariableTable` attribute for debugger/decompiler readability. Without LVT, tools show `var0`, `var1`, `var2`, `var3` instead of named variables.
+
+| Method | Slot 0 | Slot 1 | Slot 2 | Slot 3 |
+|--------|--------|--------|--------|--------|
+| `execute()` | `this` | `filterSpec` | `ctx` | `h` |
+| `_extractor()` | `this` | `_e` | `h` | — |
+| `_sink()` | `this` | `_f` | `h` | — |
+
+LVT entries are added via `PrivateMethod` inner class which carries both source code and variable descriptors.
+
+## Compile-Time Data Source Analysis
+
+The generator detects the parser type from the AST at compile time and generates typed value access:
+
+| Parser Type | LAL Example | Generated Code |
+|---|---|---|
+| JSON/YAML | `parsed.service` | `h.mapVal("service")` |
+| JSON/YAML nested | `parsed.a.b` | `h.mapVal("a", "b")` |
+| TEXT (regexp) | `parsed.level` | `h.group("level")` |
+| NONE + extraLogType | `parsed.response.code` | `((ExtraLogType) h.ctx().extraLog()).getResponse().getCode()` |
+| NONE + no extraLogType | `parsed.service` | `h.ctx().log().getService()` (LogData.Builder fallback) |
+| log fields | `log.service` | `h.ctx().log().getService()` |
+| log trace | `log.traceContext.traceId` | `h.ctx().log().getTraceContext().getTraceId()` |
+| tags | `tag("KEY")` | `h.tagValue("KEY")` |
+
+### extraLogType and LALSourceTypeProvider SPI
+
+For LAL rules with no DSL parser (`json{}`/`yaml{}`/`text{}`), the compiler needs a type to generate direct getter calls on `parsed.*` fields. Per-rule resolution order:
+
+1. **DSL parser** (`json{}`, `yaml{}`, `text{}`) — parser wins, extraLogType is ignored
+2. **Explicit `extraLogType`** in YAML rule config — FQCN string, resolved via `Class.forName()`
+3. **`LALSourceTypeProvider` SPI** — default extraLogType for a layer, discovered via `ServiceLoader`
+4. **`LogData.Builder` fallback** — if none of the above, `parsed.*` generates getter chains on `LogData.Builder` with compile-time reflection validation. Fields not found on `LogData.Builder` cause `IllegalArgumentException` at boot.
+
+The SPI interface is in `org.apache.skywalking.oap.log.analyzer.v2.spi.LALSourceTypeProvider`. Receiver plugins implement it and register in `META-INF/services/`. Example: `EnvoyHTTPLALSourceTypeProvider` registers `HTTPAccessLogEntry` for `Layer.MESH`.
+
+A single YAML file can have rules with different input types (e.g., `envoy-als.yaml` has a proto-based rule and a `json{}` rule, both in layer MESH). Resolution is per-rule, not per-file.
+
+## Example
+
+**Input**: `filter { json {} extractor { service parsed.service as String } sink {} }`
+
+One class is generated:
+
+```java
+public class LalExpr_0 implements LalExpression {
+    public void execute(FilterSpec filterSpec, ExecutionContext ctx) {
+        LalRuntimeHelper h = new LalRuntimeHelper(ctx);
+        filterSpec.json(ctx);
+        if (!ctx.shouldAbort()) {
+            _extractor(filterSpec.extractor(), h);
+        }
+        filterSpec.sink(ctx);
+    }
+    private void _extractor(ExtractorSpec _e, LalRuntimeHelper h) {
+        _e.service(h.ctx(), h.toStr(h.mapVal("service")));
+    }
+}
+```
+
+## Runtime Helper (LalRuntimeHelper)
+
+Instance-based helper created at the start of `execute()`, holds the `ExecutionContext`.
+
+**Data source methods:**
+- `mapVal(key)`, `mapVal(k1, k2)`, `mapVal(k1, k2, k3)` — JSON/YAML map access
+- `group(name)` — text regexp named group
+- `tagValue(key)` — log tag lookup
+- `ctx()` — access to ExecutionContext (for `h.ctx().log()` proto getters)
+
+**Type conversion:** `toStr()`, `toLong()`, `toInt()`, `toBool()`
+
+**Boolean evaluation:** `isTrue()`, `isNotEmpty()`
+
+**Safe navigation:** `toString()`, `trim()`
+
+## JSON/YAML LogData Field Population
+
+When `json{}` or `yaml{}` parses the log body, `FilterSpec` also adds LogData proto fields
+(`service`, `serviceInstance`, `endpoint`, `layer`, `timestamp`) to the parsed map via
+`putIfAbsent`. Body-parsed values take priority; proto fields serve as fallback. This matches
+v1 Groovy `Binding.Parsed.getAt(key)` behavior where `parsed.service` falls back to
+`LogData.getService()` when the JSON body doesn't contain a `service` key.
+
+## Null-Safe String Conversion
+
+Generated code calls `h.toStr()` instead of `String.valueOf()` for casting parsed values to String.
+This preserves Java `null` for missing fields (matching Groovy's `null as String` → `null` behavior),
+whereas `String.valueOf(null)` would produce the string `"null"`.
+
+## Data-Driven Execution Tests
+
+`LALExpressionExecutionTest` loads LAL rules from YAML and mock input from `.input.data` files:
+
+```
+test/script-cases/scripts/lal/test-lal/
+  oap-cases/                     — copies of shipped LAL configs (each with .input.data)
+  feature-cases/
+    execution-basic.yaml         — 17 LAL feature-coverage rules
+    execution-basic.input.data   — mock input + expected output per rule
+```
+
+Each `.input.data` entry specifies `body-type`, `body`, optional `tags`, and `expect` assertions
+(service, instance, endpoint, layer, tags, abort, save, timestamp, sampledTrace fields).
+
+## LAL Input Data Mock Principles
+
+LAL test data lives in `.input.data` files alongside rule YAML files under `test/script-cases/scripts/lal/`. Each entry describes one log to process and the expected output.
+
+### Input Entry Structure
+
+```yaml
+rule-name:
+  - service: test-svc               # LogData.service
+    instance: test-inst              # LogData.serviceInstance (optional)
+    body-type: json|yaml|text|none   # How to parse the body
+    body: '{"key": "value"}'         # Log body string
+    trace-id: trace-001              # Trace context (optional)
+    timestamp: 1609459200000         # LogData.timestamp (optional)
+    tags:                            # LogData tags (optional)
+      LOG_KIND: NET_PROFILING_SAMPLED_TRACE
+    extra-log:                       # For proto-typed rules (e.g., envoy-als)
+      proto-class: io.envoyproxy.envoy.data.accesslog.v3.HTTPAccessLogEntry
+      proto-json: '{"response":{"responseCode":500}}'
+    expect:                          # Expected output assertions
+      save: true                     # SinkSpec.save() called
+      abort: false                   # Not aborted
+      service: expected-svc          # Extracted service name
+      layer: MESH                    # Extracted layer
+      tag.status.code: "500"         # Extracted tag value
+      sampledTrace.traceId: trace-001  # SampledTrace field
+```
+
+### Principles
+
+1. **`body-type` determines parsing**: `json` → `json{}` block, `text` → `text{}` block, `none` → proto extraLog or raw LogData access.
+2. **`extra-log` for proto types**: When rules access `parsed.*` on protobuf types (e.g., `HTTPAccessLogEntry`), provide `proto-class` and `proto-json`. The test harness parses via `JsonFormat`.
+3. **`expect` section is mandatory**: Every entry must have `expect` with at least `save` and `abort`.
+4. **Enum fields as strings**: Fields like `sampledTrace.reason` and `sampledTrace.detectPoint` are enums. Expected values use enum names (e.g., `slow`, `client`) compared via `.name()`.
+5. **Tag assertions**: `tag.KEY` in expect asserts extracted tag values (e.g., `tag.status.code: "500"`).
+6. **SampledTrace assertions**: `sampledTrace.FIELD` asserts fields on `NativeSampledTrace` (traceId, processId, destProcessId, componentId, etc.).
+7. **v1 is the truth**: Both v1 (Groovy) and v2 (ANTLR4) must produce identical results. If v1 produces different output than expected, the expected data has a bug.
+
+### Directory Structure
+
+```
+test/script-cases/scripts/lal/test-lal/
+  oap-cases/                     — copies of shipped LAL configs
+    default.yaml / default.input.data
+    envoy-als.yaml / envoy-als.input.data
+    ...
+  feature-cases/
+    execution-basic.yaml / execution-basic.input.data  — LAL feature tests
+```
+
+## Debug Output
+
+When `SW_DYNAMIC_CLASS_ENGINE_DEBUG=true` environment variable is set, generated `.class` files are written to disk for inspection:
+
+```
+{skywalking}/lal-rt/
+  *.class          - Generated LalExpression .class files
+```
+
+This is the same env variable used by OAL. Useful for debugging code generation issues or comparing V1 vs V2 output. In tests, use `setClassOutputDir(dir)` instead.
+
+## Dependencies
+
+All within this module (grammar, compiler, and runtime are merged):
+- ANTLR4 grammar → generates lexer/parser at build time
+- `LalExpression`, `ExecutionContext`, `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..180a8435df 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,40 @@
             <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>
+        <dependency>
+            <groupId>org.apache.skywalking</groupId>
+            <artifactId>receiver-proto</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </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..b11802a616
--- /dev/null
+++ b/oap-server/analyzer/log-analyzer/src/main/antlr4/org/apache/skywalking/lal/rt/grammar/LALParser.g4
@@ -0,0 +1,476 @@
+/*
+ * 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
+    | 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
+    | L_PAREN condition R_PAREN                   # condParenGroup
+    | 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
+    : valueAccessTerm (PLUS valueAccessTerm)*
+    ;
+
+valueAccessTerm
+    : valueAccessPrimary (valueAccessSegment)*
+    ;
+
+valueAccessPrimary
+    : PARSED                                      # valueParsed
+    | LOG                                         # valueLog
+    | PROCESS_REGISTRY                            # valueProcessRegistry
+    | IDENTIFIER                                  # valueIdentifier
+    | STRING                                      # valueString
+    | NUMBER                                      # valueNumber
+    | boolValue                                   # valueBool
+    | NULL                                        # valueNull
+    | functionInvocation                          # valueFunctionCall
+    | L_PAREN valueAccess typeCast? R_PAREN       # valueParen
+    ;
+
+valueAccessSegment
+    : DOT anyIdentifier                           # segmentField
+    | QUESTION DOT anyIdentifier                  # segmentSafeField
+    | DOT functionInvocation                      # segmentMethod
+    | QUESTION DOT functionInvocation             # segmentSafeMethod
+    | L_BRACKET NUMBER R_BRACKET                  # segmentIndex
+    ;
+
+functionInvocation
+    : functionName L_PAREN functionArgList? R_PAREN
+    ;
+
+functionName
+    : IDENTIFIER
+    | TAG
+    ;
+
+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/v2/compiler/LALBlockCodegen.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALBlockCodegen.java
new file mode 100644
index 0000000000..e0b0d54605
--- /dev/null
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALBlockCodegen.java
@@ -0,0 +1,1157 @@
+/*
+ * 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.v2.compiler;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.apache.skywalking.apm.network.logging.v3.LogData;
+
+/**
+ * Static code-generation methods for LAL extractor, sink, condition, and
+ * value-access blocks. Extracted from {@link LALClassGenerator} for
+ * readability; all methods are stateless and take a
+ * {@link LALClassGenerator.GenCtx} parameter for shared state.
+ */
+final class LALBlockCodegen {
+
+    private static final String FILTER_SPEC =
+        "org.apache.skywalking.oap.log.analyzer.v2.dsl.spec.filter.FilterSpec";
+    private static final String EXTRACTOR_SPEC =
+        "org.apache.skywalking.oap.log.analyzer.v2.dsl.spec.extractor.ExtractorSpec";
+    private static final String SAMPLE_BUILDER =
+        EXTRACTOR_SPEC + "$SampleBuilder";
+    private static final String H =
+        "org.apache.skywalking.oap.log.analyzer.v2.compiler.rt.LalRuntimeHelper";
+    private static final String PROCESS_REGISTRY =
+        "org.apache.skywalking.oap.meter.analyzer.v2.dsl.registry.ProcessRegistry";
+
+    private LALBlockCodegen() {
+        // utility class
+    }
+
+    // ==================== Extractor method generation ====================
+
+    static void generateExtractorMethod(final StringBuilder sb,
+                                         final LALScriptModel.ExtractorBlock block,
+                                         final LALClassGenerator.GenCtx genCtx) {
+        final String methodName = genCtx.nextMethodName("extractor");
+        final Object[] savedState = genCtx.saveProtoVarState();
+        genCtx.resetProtoVars();
+
+        // Generate body first to collect proto var declarations
+        final StringBuilder bodyContent = new StringBuilder();
+        final List<LALScriptModel.FilterStatement> extractorStmts = new ArrayList<>();
+        for (final LALScriptModel.ExtractorStatement es : block.getStatements()) {
+            extractorStmts.add((LALScriptModel.FilterStatement) es);
+        }
+        generateExtractorBody(bodyContent, extractorStmts, genCtx);
+
+        // Assemble method with declarations before body
+        final StringBuilder body = new StringBuilder();
+        body.append("private void ").append(methodName).append("(")
+            .append(EXTRACTOR_SPEC).append(" _e, ").append(H).append(" h) {\n");
+
+        final List<String[]> lvtVars = new ArrayList<>();
+        lvtVars.add(new String[]{"_e", "L" + EXTRACTOR_SPEC.replace('.', '/') + ";"});
+        lvtVars.add(new String[]{"h", "L" + H.replace('.', '/') + ";"});
+
+        if (genCtx.usedProtoAccess) {
+            if (genCtx.extraLogType != null) {
+                final String elTypeName = genCtx.extraLogType.getName();
+                body.append("  ").append(elTypeName).append(" _p = (")
+                    .append(elTypeName).append(") h.ctx().extraLog();\n");
+                lvtVars.add(new String[]{"_p",
+                    "L" + elTypeName.replace('.', '/') + ";"});
+            }
+            body.append(genCtx.protoVarDecls);
+            lvtVars.addAll(genCtx.protoLvtVars);
+        }
+
+        body.append(bodyContent);
+        body.append("}\n");
+        genCtx.privateMethods.add(new LALClassGenerator.PrivateMethod(
+            body.toString(), lvtVars.toArray(new String[0][])));
+
+        genCtx.restoreProtoVarState(savedState);
+
+        sb.append("  if (!ctx.shouldAbort()) {\n");
+        sb.append("    ").append(methodName).append("(filterSpec.extractor(), h);\n");
+        sb.append("  }\n");
+    }
+
+    static void generateExtractorBody(
+            final StringBuilder sb,
+            final List<? extends LALScriptModel.FilterStatement> stmts,
+            final LALClassGenerator.GenCtx genCtx) {
+        for (final LALScriptModel.FilterStatement stmt : stmts) {
+            if (stmt instanceof LALScriptModel.FieldAssignment) {
+                final LALScriptModel.FieldAssignment field =
+                    (LALScriptModel.FieldAssignment) stmt;
+                sb.append("  _e.").append(field.getFieldType().name().toLowerCase())
+                  .append("(h.ctx(), ");
+                generateCastedValueAccess(sb, field.getValue(),
+                    field.getCastType(), genCtx);
+                if (field.getFormatPattern() != null) {
+                    sb.append(", \"")
+                      .append(LALCodegenHelper.escapeJava(field.getFormatPattern()))
+                      .append("\"");
+                }
+                sb.append(");\n");
+            } else if (stmt instanceof LALScriptModel.TagAssignment) {
+                generateTagAssignment(sb, (LALScriptModel.TagAssignment) stmt, genCtx);
+            } else if (stmt instanceof LALScriptModel.IfBlock) {
+                generateIfBlockInExtractor(sb, (LALScriptModel.IfBlock) stmt, genCtx);
+            } else if (stmt instanceof LALScriptModel.MetricsBlock) {
+                generateMetricsInline(sb, (LALScriptModel.MetricsBlock) stmt, genCtx);
+            } else if (stmt instanceof LALScriptModel.SlowSqlBlock) {
+                generateSlowSqlInline(sb, (LALScriptModel.SlowSqlBlock) stmt, genCtx);
+            } else if (stmt instanceof LALScriptModel.SampledTraceBlock) {
+                generateSampledTraceInline(sb,
+                    (LALScriptModel.SampledTraceBlock) stmt, genCtx);
+            }
+        }
+    }
+
+    static void generateIfBlockInExtractor(
+            final StringBuilder sb,
+            final LALScriptModel.IfBlock ifBlock,
+            final LALClassGenerator.GenCtx genCtx) {
+        sb.append("  if (");
+        generateCondition(sb, ifBlock.getCondition(), genCtx);
+        sb.append(") {\n");
+        generateExtractorBody(sb, ifBlock.getThenBranch(), genCtx);
+        sb.append("  }\n");
+        if (!ifBlock.getElseBranch().isEmpty()) {
+            sb.append("  else {\n");
+            generateExtractorBody(sb, ifBlock.getElseBranch(), genCtx);
+            sb.append("  }\n");
+        }
+    }
+
+    // ==================== Metrics inline ====================
+
+    static void generateMetricsInline(
+            final StringBuilder sb,
+            final LALScriptModel.MetricsBlock block,
+            final LALClassGenerator.GenCtx genCtx) {
+        sb.append("  { ").append(SAMPLE_BUILDER).append(" _b = _e.prepareMetrics(h.ctx());\n");
+        sb.append("  if (_b != null) {\n");
+        if (block.getName() != null) {
+            sb.append("  _b.name(\"")
+                .append(LALCodegenHelper.escapeJava(block.getName())).append("\");\n");
+        }
+        if (block.getTimestampValue() != null) {
+            sb.append("  _b.timestamp(");
+            generateCastedValueAccess(sb, block.getTimestampValue(),
+                block.getTimestampCast(), genCtx);
+            sb.append(");\n");
+        }
+        if (!block.getLabels().isEmpty()) {
+            sb.append("  { java.util.Map _labels = new java.util.LinkedHashMap();\n");
+            for (final Map.Entry<String, LALScriptModel.TagValue> entry
+                    : block.getLabels().entrySet()) {
+                sb.append("    _labels.put(\"")
+                    .append(LALCodegenHelper.escapeJava(entry.getKey())).append("\", ");
+                generateCastedValueAccess(sb, entry.getValue().getValue(),
+                    entry.getValue().getCastType(), genCtx);
+                sb.append(");\n");
+            }
+            sb.append("    _b.labels(_labels); }\n");
+        }
+        if (block.getValue() != null) {
+            sb.append("  _b.value(");
+            if ("Long".equals(block.getValueCast())) {
+                sb.append("(double) h.toLong(");
+                generateValueAccess(sb, block.getValue(), genCtx);
+                sb.append(")");
+            } else if ("Integer".equals(block.getValueCast())) {
+                sb.append("(double) h.toInt(");
+                generateValueAccess(sb, block.getValue(), genCtx);
+                sb.append(")");
+            } else {
+                if (block.getValue().isNumberLiteral()) {
+                    sb.append("(double) ").append(block.getValue().getSegments().get(0));
+                } else {
+                    sb.append("((Number) ");
+                    generateValueAccess(sb, block.getValue(), genCtx);
+                    sb.append(").doubleValue()");
+                }
+            }
+            sb.append(");\n");
+        }
+        sb.append("  _e.submitMetrics(h.ctx(), _b);\n");
+        sb.append("  } }\n");
+    }
+
+    // ==================== SlowSql inline ====================
+
+    static void generateSlowSqlInline(
+            final StringBuilder sb,
+            final LALScriptModel.SlowSqlBlock block,
+            final LALClassGenerator.GenCtx genCtx) {
+        sb.append("  _e.prepareSlowSql(h.ctx());\n");
+        if (block.getId() != null) {
+            sb.append("  _e.slowSqlSpec().id(h.ctx(), ");
+            generateCastedValueAccess(sb, block.getId(), block.getIdCast(), genCtx);
+            sb.append(");\n");
+        }
+        if (block.getStatement() != null) {
+            sb.append("  _e.slowSqlSpec().statement(h.ctx(), ");
+            generateCastedValueAccess(sb, block.getStatement(),
+                block.getStatementCast(), genCtx);
+            sb.append(");\n");
+        }
+        if (block.getLatency() != null) {
+            sb.append("  _e.slowSqlSpec().latency(h.ctx(), Long.valueOf(h.toLong(");
+            generateValueAccess(sb, block.getLatency(), genCtx);
+            sb.append(")));\n");
+        }
+        sb.append("  _e.submitSlowSql(h.ctx());\n");
+    }
+
+    // ==================== SampledTrace inline ====================
+
+    static void generateSampledTraceInline(
+            final StringBuilder sb,
+            final LALScriptModel.SampledTraceBlock block,
+            final LALClassGenerator.GenCtx genCtx) {
+        sb.append("  _e.prepareSampledTrace(h.ctx());\n");
+        generateSampledTraceBody(sb, block.getStatements(), genCtx);
+        sb.append("  _e.submitSampledTrace(h.ctx());\n");
+    }
+
+    static void generateSampledTraceBody(
+            final StringBuilder sb,
+            final List<LALScriptModel.SampledTraceStatement> stmts,
+            final LALClassGenerator.GenCtx genCtx) {
+        for (final LALScriptModel.SampledTraceStatement stmt : stmts) {
+            if (stmt instanceof LALScriptModel.SampledTraceField) {
+                generateSampledTraceField(sb, (LALScriptModel.SampledTraceField) stmt,
+                    genCtx);
+            } else if (stmt instanceof LALScriptModel.IfBlock) {
+                generateSampledTraceIfBlock(sb, (LALScriptModel.IfBlock) stmt, genCtx);
+            }
+        }
+    }
+
+    static void generateSampledTraceField(
+            final StringBuilder sb,
+            final LALScriptModel.SampledTraceField field,
+            final LALClassGenerator.GenCtx genCtx) {
+        switch (field.getFieldType()) {
+            case LATENCY:
+                sb.append("  _e.sampledTraceSpec().latency(h.ctx(), Long.valueOf(h.toLong(");
+                generateValueAccess(sb, field.getValue(), genCtx);
+                sb.append(")));\n");
+                return;
+            case COMPONENT_ID:
+                sb.append("  _e.sampledTraceSpec().componentId(h.ctx(), h.toInt(");
+                generateValueAccess(sb, field.getValue(), genCtx);
+                sb.append("));\n");
+                return;
+            case URI:
+                sb.append("  _e.sampledTraceSpec().uri(h.ctx(), ");
+                break;
+            case REASON:
+                sb.append("  _e.sampledTraceSpec().reason(h.ctx(), ");
+                break;
+            case PROCESS_ID:
+                sb.append("  _e.sampledTraceSpec().processId(h.ctx(), ");
+                break;
+            case DEST_PROCESS_ID:
+                sb.append("  _e.sampledTraceSpec().destProcessId(h.ctx(), ");
+                break;
+            case DETECT_POINT:
+                sb.append("  _e.sampledTraceSpec().detectPoint(h.ctx(), ");
+                break;
+            case REPORT_SERVICE:
+                sb.append("  _e.sampledTraceSpec().")
+                  .append(field.getFieldType().name().toLowerCase())
+                  .append("(h.ctx(), ");
+                break;
+            default:
+                return;
+        }
+        generateCastedValueAccess(sb, field.getValue(), field.getCastType(), genCtx);
+        sb.append(");\n");
+    }
+
+    static void generateSampledTraceIfBlock(
+            final StringBuilder sb,
+            final LALScriptModel.IfBlock ifBlock,
+            final LALClassGenerator.GenCtx genCtx) {
+        sb.append("  if (");
+        generateCondition(sb, ifBlock.getCondition(), genCtx);
+        sb.append(") {\n");
+        generateSampledTraceBodyFromFilterStmts(sb, ifBlock.getThenBranch(), genCtx);
+        sb.append("  }\n");
+        if (!ifBlock.getElseBranch().isEmpty()) {
+            sb.append("  else {\n");
+            generateSampledTraceBodyFromFilterStmts(sb, ifBlock.getElseBranch(), genCtx);
+            sb.append("  }\n");
+        }
+    }
+
+    static void generateSampledTraceBodyFromFilterStmts(
+            final StringBuilder sb,
+            final List<? extends LALScriptModel.FilterStatement> stmts,
+            final LALClassGenerator.GenCtx genCtx) {
+        for (final LALScriptModel.FilterStatement stmt : stmts) {
+            if (stmt instanceof LALScriptModel.SampledTraceField) {
+                generateSampledTraceField(sb,
+                    (LALScriptModel.SampledTraceField) stmt, genCtx);
+            } else if (stmt instanceof LALScriptModel.FieldAssignment) {
+                generateSampledTraceFieldFromAssignment(sb,
+                    (LALScriptModel.FieldAssignment) stmt, genCtx);
+            } else if (stmt instanceof LALScriptModel.IfBlock) {
+                generateSampledTraceIfBlock(sb, (LALScriptModel.IfBlock) stmt, genCtx);
+            }
+        }
+    }
+
+    static void generateSampledTraceFieldFromAssignment(
+            final StringBuilder sb,
+            final LALScriptModel.FieldAssignment fa,
+            final LALClassGenerator.GenCtx genCtx) {
+        switch (fa.getFieldType()) {
+            case TIMESTAMP:
+                sb.append("  _e.sampledTraceSpec().latency(h.ctx(), Long.valueOf(h.toLong(");
+                generateValueAccess(sb, fa.getValue(), genCtx);
+                sb.append(")));\n");
+                break;
+            default:
+                sb.append("  _e.sampledTraceSpec().")
+                  .append(fa.getFieldType().name().toLowerCase())
+                  .append("(h.ctx(), ");
+                generateCastedValueAccess(sb, fa.getValue(), fa.getCastType(), genCtx);
+                sb.append(");\n");
+                break;
+        }
+    }
+
+    // ==================== Tag assignment ====================
+
+    static void generateTagAssignment(final StringBuilder sb,
+                                       final LALScriptModel.TagAssignment tag,
+                                       final LALClassGenerator.GenCtx genCtx) {
+        for (final Map.Entry<String, LALScriptModel.TagValue> entry
+                : tag.getTags().entrySet()) {
+            sb.append("  _e.tag(h.ctx(), \"")
+              .append(LALCodegenHelper.escapeJava(entry.getKey())).append("\", ");
+            generateStringValueAccess(sb, entry.getValue().getValue(),
+                entry.getValue().getCastType(), genCtx);
+            sb.append(");\n");
+        }
+    }
+
+    // ==================== Sink method generation ====================
+
+    static void generateSinkMethod(final StringBuilder sb,
+                                    final LALScriptModel.SinkBlock sink,
+                                    final LALClassGenerator.GenCtx genCtx) {
+        final String methodName = genCtx.nextMethodName("sink");
+        final Object[] savedState = genCtx.saveProtoVarState();
+        genCtx.resetProtoVars();
+
+        // Generate body first to collect proto var declarations
+        final StringBuilder bodyContent = new StringBuilder();
+        final List<LALScriptModel.FilterStatement> sinkStmts = new ArrayList<>();
+        for (final LALScriptModel.SinkStatement ss : sink.getStatements()) {
+            sinkStmts.add((LALScriptModel.FilterStatement) ss);
+        }
+        generateSinkBody(bodyContent, sinkStmts, genCtx);
+
+        // Assemble method with declarations before body
+        final StringBuilder body = new StringBuilder();
+        body.append("private void ").append(methodName).append("(")
+            .append(FILTER_SPEC).append(" _f, ").append(H).append(" h) {\n");
+
+        final List<String[]> lvtVars = new ArrayList<>();
+        lvtVars.add(new String[]{"_f", "L" + FILTER_SPEC.replace('.', '/') + ";"});
+        lvtVars.add(new String[]{"h", "L" + H.replace('.', '/') + ";"});
+
+        if (genCtx.usedProtoAccess) {
+            if (genCtx.extraLogType != null) {
+                final String elTypeName = genCtx.extraLogType.getName();
+                body.append("  ").append(elTypeName).append(" _p = (")
+                    .append(elTypeName).append(") h.ctx().extraLog();\n");
+                lvtVars.add(new String[]{"_p",
+                    "L" + elTypeName.replace('.', '/') + ";"});
+            }
+            body.append(genCtx.protoVarDecls);
+            lvtVars.addAll(genCtx.protoLvtVars);
+        }
+
+        body.append(bodyContent);
+        body.append("}\n");
+        genCtx.privateMethods.add(new LALClassGenerator.PrivateMethod(
+            body.toString(), lvtVars.toArray(new String[0][])));
+
+        genCtx.restoreProtoVarState(savedState);
+
+        sb.append("  if (!ctx.shouldAbort()) {\n");
+        sb.append("    ").append(methodName).append("(filterSpec, h);\n");
+        sb.append("  }\n");
+        sb.append("  filterSpec.finalizeSink(ctx);\n");
+    }
+
+    static void generateSinkBody(
+            final StringBuilder sb,
+            final List<? extends LALScriptModel.FilterStatement> stmts,
+            final LALClassGenerator.GenCtx genCtx) {
+        for (final LALScriptModel.FilterStatement stmt : stmts) {
+            if (stmt instanceof LALScriptModel.EnforcerStatement) {
+                sb.append("  _f.enforcer(h.ctx());\n");
+            } else if (stmt instanceof LALScriptModel.DropperStatement) {
+                sb.append("  _f.dropper(h.ctx());\n");
+            } else if (stmt instanceof LALScriptModel.SamplerBlock) {
+                generateSamplerInline(sb, (LALScriptModel.SamplerBlock) stmt, genCtx);
+            } else if (stmt instanceof LALScriptModel.IfBlock) {
+                generateIfBlockInSink(sb, (LALScriptModel.IfBlock) stmt, genCtx);
+            }
+        }
+    }
+
+    static void generateIfBlockInSink(
+            final StringBuilder sb,
+            final LALScriptModel.IfBlock ifBlock,
+            final LALClassGenerator.GenCtx genCtx) {
+        sb.append("  if (");
+        generateCondition(sb, ifBlock.getCondition(), genCtx);
+        sb.append(") {\n");
+        generateSinkBody(sb, ifBlock.getThenBranch(), genCtx);
+        sb.append("  }\n");
+        if (!ifBlock.getElseBranch().isEmpty()) {
+            sb.append("  else {\n");
+            generateSinkBody(sb, ifBlock.getElseBranch(), genCtx);
+            sb.append("  }\n");
+        }
+    }
+
+    // ==================== Sampler/RateLimit inline ====================
+
+    static void generateSamplerInline(
+            final StringBuilder sb,
+            final LALScriptModel.SamplerBlock block,
+            final LALClassGenerator.GenCtx genCtx) {
+        generateSamplerContents(sb, block.getContents(), genCtx);
+    }
+
+    static void generateSamplerContents(
+            final StringBuilder sb,
+            final List<LALScriptModel.SamplerContent> contents,
+            final LALClassGenerator.GenCtx genCtx) {
+        for (final LALScriptModel.SamplerContent content : contents) {
+            if (content instanceof LALScriptModel.RateLimitBlock) {
+                generateRateLimitInline(sb, (LALScriptModel.RateLimitBlock) content,
+                    genCtx);
+            } else if (content instanceof LALScriptModel.IfBlock) {
+                generateSamplerIfBlock(sb, (LALScriptModel.IfBlock) content, genCtx);
+            }
+        }
+    }
+
+    static void generateSamplerIfBlock(
+            final StringBuilder sb,
+            final LALScriptModel.IfBlock ifBlock,
+            final LALClassGenerator.GenCtx genCtx) {
+        sb.append("  if (");
+        generateCondition(sb, ifBlock.getCondition(), genCtx);
+        sb.append(") {\n");
+        generateSamplerContentsFromFilterStmts(sb, ifBlock.getThenBranch(), genCtx);
+        sb.append("  }\n");
+        if (!ifBlock.getElseBranch().isEmpty()) {
+            sb.append("  else {\n");
+            generateSamplerContentsFromFilterStmts(sb, ifBlock.getElseBranch(), genCtx);
+            sb.append("  }\n");
+        }
+    }
+
+    static void generateSamplerContentsFromFilterStmts(
+            final StringBuilder sb,
+            final List<? extends LALScriptModel.FilterStatement> stmts,
+            final LALClassGenerator.GenCtx genCtx) {
+        for (final LALScriptModel.FilterStatement stmt : stmts) {
+            if (stmt instanceof LALScriptModel.SamplerBlock) {
+                generateSamplerContents(sb,
+                    ((LALScriptModel.SamplerBlock) stmt).getContents(), genCtx);
+            } else if (stmt instanceof LALScriptModel.IfBlock) {
+                generateSamplerIfBlock(sb, (LALScriptModel.IfBlock) stmt, genCtx);
+            }
+        }
+    }
+
+    static void generateRateLimitInline(
+            final StringBuilder sb,
+            final LALScriptModel.RateLimitBlock block,
+            final LALClassGenerator.GenCtx genCtx) {
+        sb.append("  _f.sampler().rateLimit(h.ctx(), ");
+        if (block.isIdInterpolated()) {
+            sb.append("\"\"");
+            for (final LALScriptModel.InterpolationPart part : block.getIdParts()) {
+                sb.append(" + ");
+                if (part.isLiteral()) {
+                    sb.append("\"").append(LALCodegenHelper.escapeJava(part.getLiteral()))
+                      .append("\"");
+                } else {
+                    sb.append("String.valueOf(");
+                    generateValueAccess(sb, part.getExpression(), genCtx);
+                    sb.append(")");
+                }
+            }
+        } else {
+            sb.append("\"").append(LALCodegenHelper.escapeJava(block.getId())).append("\"");
+        }
+        sb.append(", ").append(block.getRpm()).append(");\n");
+    }
+
+    // ==================== Conditions ====================
+
+    static void generateCondition(final StringBuilder sb,
+                                   final LALScriptModel.Condition cond,
+                                   final LALClassGenerator.GenCtx genCtx) {
+        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(), genCtx);
+                    sb.append(", ");
+                    generateConditionValue(sb, cc.getRight(), genCtx);
+                    sb.append(")");
+                    break;
+                case NEQ:
+                    sb.append("!java.util.Objects.equals(");
+                    generateValueAccessObj(sb, cc.getLeft(), cc.getLeftCast(), genCtx);
+                    sb.append(", ");
+                    generateConditionValue(sb, cc.getRight(), genCtx);
+                    sb.append(")");
+                    break;
+                case GT:
+                    generateNumericComparison(sb, cc, " > ", genCtx);
+                    break;
+                case LT:
+                    generateNumericComparison(sb, cc, " < ", genCtx);
+                    break;
+                case GTE:
+                    generateNumericComparison(sb, cc, " >= ", genCtx);
+                    break;
+                case LTE:
+                    generateNumericComparison(sb, cc, " <= ", genCtx);
+                    break;
+                default:
+                    break;
+            }
+        } else if (cond instanceof LALScriptModel.LogicalCondition) {
+            final LALScriptModel.LogicalCondition lc =
+                (LALScriptModel.LogicalCondition) cond;
+            sb.append("(");
+            generateCondition(sb, lc.getLeft(), genCtx);
+            sb.append(lc.getOp() == LALScriptModel.LogicalOp.AND
+                ? " && " : " || ");
+            generateCondition(sb, lc.getRight(), genCtx);
+            sb.append(")");
+        } else if (cond instanceof LALScriptModel.NotCondition) {
+            sb.append("!(");
+            generateCondition(sb,
+                ((LALScriptModel.NotCondition) cond).getInner(), genCtx);
+            sb.append(")");
+        } else if (cond instanceof LALScriptModel.ExprCondition) {
+            final String ct = ((LALScriptModel.ExprCondition) cond).getCastType();
+            final String method = "Boolean".equals(ct) || "boolean".equals(ct)
+                ? ".isTrue(" : ".isNotEmpty(";
+            sb.append("h").append(method);
+            generateValueAccessObj(sb,
+                ((LALScriptModel.ExprCondition) cond).getExpr(),
+                ct, genCtx);
+            sb.append(")");
+        }
+    }
+
+    static void generateNumericComparison(
+            final StringBuilder sb,
+            final LALScriptModel.ComparisonCondition cc,
+            final String op,
+            final LALClassGenerator.GenCtx genCtx) {
+        // Generate left side into buffer to inspect resolved type
+        final StringBuilder leftBuf = new StringBuilder();
+        generateValueAccessObj(leftBuf, cc.getLeft(), null, genCtx);
+
+        final boolean primitiveNumeric = genCtx.lastResolvedType != null
+            && (genCtx.lastResolvedType == int.class
+                || genCtx.lastResolvedType == long.class);
+
+        if (primitiveNumeric && genCtx.lastRawChain != null) {
+            // Direct primitive comparison — no boxing, no h.toLong()
+            if (genCtx.lastNullChecks != null) {
+                sb.append("(").append(genCtx.lastNullChecks).append(" ? false : ")
+                  .append(genCtx.lastRawChain).append(op);
+                generateConditionValueNumeric(sb, cc.getRight(), genCtx);
+                sb.append(")");
+            } else {
+                sb.append(genCtx.lastRawChain).append(op);
+                generateConditionValueNumeric(sb, cc.getRight(), genCtx);
+            }
+        } else {
+            // Fallback: h.toLong() conversion
+            sb.append("h.toLong(").append(leftBuf).append(")").append(op);
+            generateConditionValueNumeric(sb, cc.getRight(), genCtx);
+        }
+    }
+
+    static void generateConditionValue(final StringBuilder sb,
+                                        final LALScriptModel.ConditionValue cv,
+                                        final LALClassGenerator.GenCtx genCtx) {
+        if (cv instanceof LALScriptModel.StringConditionValue) {
+            sb.append('"')
+              .append(LALCodegenHelper.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, genCtx);
+        }
+    }
+
+    static void generateConditionValueNumeric(
+            final StringBuilder sb,
+            final LALScriptModel.ConditionValue cv,
+            final LALClassGenerator.GenCtx genCtx) {
+        if (cv instanceof LALScriptModel.NumberConditionValue) {
+            sb.append((long) ((LALScriptModel.NumberConditionValue) cv)
+                .getValue()).append("L");
+        } else if (cv instanceof LALScriptModel.ValueAccessConditionValue) {
+            sb.append("h.toLong(");
+            generateValueAccessObj(sb,
+                ((LALScriptModel.ValueAccessConditionValue) cv).getValue(),
+                null, genCtx);
+            sb.append(")");
+        } else {
+            sb.append("0L");
+        }
+    }
+
+    // ==================== Value access ====================
+
+    static void generateCastedValueAccess(final StringBuilder sb,
+                                           final LALScriptModel.ValueAccess value,
+                                           final String castType,
+                                           final LALClassGenerator.GenCtx genCtx) {
+        if ("String".equals(castType)) {
+            sb.append("h.toStr(");
+            generateValueAccess(sb, value, genCtx);
+            sb.append(")");
+        } else if ("Long".equals(castType)) {
+            sb.append("h.toLong(");
+            generateValueAccess(sb, value, genCtx);
+            sb.append(")");
+        } else if ("Integer".equals(castType)) {
+            sb.append("h.toInt(");
+            generateValueAccess(sb, value, genCtx);
+            sb.append(")");
+        } else if ("Boolean".equals(castType)) {
+            sb.append("h.toBool(");
+            generateValueAccess(sb, value, genCtx);
+            sb.append(")");
+        } else {
+            generateValueAccess(sb, value, genCtx);
+        }
+    }
+
+    static void generateStringValueAccess(final StringBuilder sb,
+                                            final LALScriptModel.ValueAccess value,
+                                            final String castType,
+                                            final LALClassGenerator.GenCtx genCtx) {
+        if (castType == null || "String".equals(castType)) {
+            sb.append("h.toStr(");
+            generateValueAccess(sb, value, genCtx);
+            sb.append(")");
+        } else if ("Long".equals(castType)) {
+            sb.append("String.valueOf(h.toLong(");
+            generateValueAccess(sb, value, genCtx);
+            sb.append("))");
+        } else if ("Integer".equals(castType)) {
+            sb.append("String.valueOf(h.toInt(");
+            generateValueAccess(sb, value, genCtx);
+            sb.append("))");
+        } else if ("Boolean".equals(castType)) {
+            sb.append("String.valueOf(h.toBool(");
+            generateValueAccess(sb, value, genCtx);
+            sb.append("))");
+        } else {
+            sb.append("h.toStr(");
+            generateValueAccess(sb, value, genCtx);
+            sb.append(")");
+        }
+    }
+
+    static void generateValueAccessObj(final StringBuilder sb,
+                                        final LALScriptModel.ValueAccess value,
+                                        final String castType,
+                                        final LALClassGenerator.GenCtx genCtx) {
+        if ("String".equals(castType)) {
+            sb.append("h.toStr(");
+            generateValueAccess(sb, value, genCtx);
+            sb.append(")");
+        } else {
+            generateValueAccess(sb, value, genCtx);
+        }
+    }
+
+    static void generateValueAccess(final StringBuilder sb,
+                                     final LALScriptModel.ValueAccess value,
+                                     final LALClassGenerator.GenCtx genCtx) {
+        genCtx.clearExtraLogResult();
+
+        // Handle string concatenation (term1 + term2 + ...)
+        if (!value.getConcatParts().isEmpty()) {
+            sb.append("(\"\" + ");
+            for (int i = 0; i < value.getConcatParts().size(); i++) {
+                if (i > 0) {
+                    sb.append(" + ");
+                }
+                generateValueAccess(sb, value.getConcatParts().get(i), genCtx);
+            }
+            sb.append(")");
+            return;
+        }
+
+        // Handle parenthesized expression: (innerExpr as Type).chain...
+        if (value.getParenInner() != null) {
+            generateParenAccess(sb, value, genCtx);
+            return;
+        }
+
+        // Handle function call primaries (e.g., tag("LOG_KIND"))
+        if (value.getFunctionCallName() != null) {
+            if ("tag".equals(value.getFunctionCallName())
+                    && !value.getFunctionCallArgs().isEmpty()) {
+                sb.append("h.tagValue(\"");
+                final String key = value.getFunctionCallArgs().get(0)
+                    .getValue().getSegments().get(0);
+                sb.append(LALCodegenHelper.escapeJava(key)).append("\")");
+            } else {
+                sb.append("null");
+            }
+            return;
+        }
+
+        // Handle string/number literals
+        if (value.isStringLiteral() && value.getChain().isEmpty()) {
+            sb.append("\"").append(LALCodegenHelper.escapeJava(value.getSegments().get(0)))
+              .append("\"");
+            return;
+        }
+        if (value.isNumberLiteral() && value.getChain().isEmpty()) {
+            final String num = value.getSegments().get(0);
+            if (num.contains(".")) {
+                sb.append("Double.valueOf(").append(num).append(")");
+            } else {
+                sb.append("Integer.valueOf(").append(num).append(")");
+            }
+            return;
+        }
+
+        // Handle ProcessRegistry static calls
+        if (value.isProcessRegistryRef()) {
+            generateProcessRegistryCall(sb, value, genCtx);
+            return;
+        }
+
+        final List<LALScriptModel.ValueAccessSegment> chain = value.getChain();
+
+        // Handle log.X.Y direct proto getter chains
+        if (value.isLogRef()) {
+            generateLogAccess(sb, chain);
+            return;
+        }
+
+        // Handle parsed.X.Y with compile-time type analysis
+        if (value.isParsedRef()) {
+            generateParsedAccess(sb, chain, genCtx);
+            return;
+        }
+
+        // Fallback for unknown primary
+        if (chain.isEmpty()) {
+            sb.append("null");
+            return;
+        }
+        // Treat as parsed ref
+        generateParsedAccess(sb, chain, genCtx);
+    }
+
+    // ==================== Parenthesized expression ====================
+
+    static void generateParenAccess(final StringBuilder sb,
+                                     final LALScriptModel.ValueAccess value,
+                                     final LALClassGenerator.GenCtx genCtx) {
+        // Generate the inner expression with cast
+        final String castType = value.getParenCast();
+        final StringBuilder inner = new StringBuilder();
+        if (castType != null) {
+            generateCastedValueAccess(inner, value.getParenInner(), castType, genCtx);
+        } else {
+            generateValueAccess(inner, value.getParenInner(), genCtx);
+        }
+
+        // Apply chain segments (methods, fields, index access)
+        String current = inner.toString();
+        for (final LALScriptModel.ValueAccessSegment seg : value.getChain()) {
+            if (seg instanceof LALScriptModel.MethodSegment) {
+                current = appendMethodSegment(current,
+                    (LALScriptModel.MethodSegment) seg);
+            } else if (seg instanceof LALScriptModel.IndexSegment) {
+                current = current + "["
+                    + ((LALScriptModel.IndexSegment) seg).getIndex() + "]";
+            } else if (seg instanceof LALScriptModel.FieldSegment) {
+                final LALScriptModel.FieldSegment fs =
+                    (LALScriptModel.FieldSegment) seg;
+                if (fs.isSafeNav()) {
+                    current = "(" + current + " == null ? null : "
+                        + current + "." + fs.getName() + ")";
+                } else {
+                    current = current + "." + fs.getName();
+                }
+            }
+        }
+        sb.append(current);
+    }
+
+    // ==================== Log access (direct proto getters) ====================
+
+    static void generateLogAccess(final StringBuilder sb,
+                                   final List<LALScriptModel.ValueAccessSegment> chain) {
+        if (chain.isEmpty()) {
+            sb.append("h.ctx().log()");
+            return;
+        }
+
+        String current = "h.ctx().log()";
+        boolean needsBoxing = false;
+        String boxType = null;
+
+        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();
+                if (i == 0 && LALCodegenHelper.LOG_GETTERS.containsKey(name)) {
+                    if ("traceContext".equals(name)) {
+                        current = current + ".getTraceContext()";
+                    } else {
+                        current = current + "."
+                            + LALCodegenHelper.LOG_GETTERS.get(name) + "()";
+                        if (LALCodegenHelper.LONG_FIELDS.contains(name)) {
+                            needsBoxing = true;
+                            boxType = "Long";
+                        }
+                    }
+                } else if (i == 1 && current.endsWith(".getTraceContext()")
+                        && LALCodegenHelper.TRACE_CONTEXT_GETTERS.containsKey(name)) {
+                    current = current + "."
+                        + LALCodegenHelper.TRACE_CONTEXT_GETTERS.get(name) + "()";
+                    if (LALCodegenHelper.INT_FIELDS.contains(name)) {
+                        needsBoxing = true;
+                        boxType = "Integer";
+                    }
+                } else {
+                    throw new IllegalArgumentException(
+                        "Unknown log field: log." + name
+                            + ". Supported fields: "
+                            + LALCodegenHelper.LOG_GETTERS.keySet()
+                            + ", traceContext."
+                            + LALCodegenHelper.TRACE_CONTEXT_GETTERS.keySet());
+                }
+            } else if (seg instanceof LALScriptModel.MethodSegment) {
+                current = appendMethodSegment(current,
+                    (LALScriptModel.MethodSegment) seg);
+            }
+        }
+
+        if (needsBoxing) {
+            sb.append(boxType).append(".valueOf(").append(current).append(")");
+        } else {
+            sb.append(current);
+        }
+    }
+
+    // ==================== Parsed access (compile-time typed) ====================
+
+    static void generateParsedAccess(
+            final StringBuilder sb,
+            final List<LALScriptModel.ValueAccessSegment> chain,
+            final LALClassGenerator.GenCtx genCtx) {
+        if (chain.isEmpty()) {
+            sb.append("h.ctx().parsed()");
+            return;
+        }
+
+        // Collect leading field segments (stop at method/index)
+        final List<LALScriptModel.FieldSegment> fieldSegments = new ArrayList<>();
+        int methodStart = -1;
+        for (int i = 0; i < chain.size(); i++) {
+            final LALScriptModel.ValueAccessSegment seg = chain.get(i);
+            if (seg instanceof LALScriptModel.FieldSegment) {
+                fieldSegments.add((LALScriptModel.FieldSegment) seg);
+            } else {
+                methodStart = i;
+                break;
+            }
+        }
+
+        final List<String> fieldKeys = new ArrayList<>();
+        for (final LALScriptModel.FieldSegment fs : fieldSegments) {
+            fieldKeys.add(fs.getName());
+        }
+
+        String current;
+        switch (genCtx.parserType) {
+            case JSON:
+            case YAML:
+                current = LALCodegenHelper.generateMapValCall(fieldKeys);
+                break;
+            case TEXT:
+                if (!fieldKeys.isEmpty()) {
+                    current = "h.group(\""
+                        + LALCodegenHelper.escapeJava(fieldKeys.get(0)) + "\")";
+                } else {
+                    current = "h.ctx().parsed()";
+                }
+                break;
+            case NONE:
+                if (genCtx.extraLogType != null) {
+                    current = generateExtraLogAccess(fieldSegments, genCtx.extraLogType,
+                        "_p", true, genCtx);
+                } else {
+                    // No parser and no extraLogType — fall back to LogData proto
+                    current = generateExtraLogAccess(fieldSegments, LogData.Builder.class,
+                        "h.ctx().log()", false, genCtx);
+                }
+                break;
+            default:
+                current = "null";
+                break;
+        }
+
+        // Apply remaining method/index segments
+        if (methodStart >= 0) {
+            for (int i = methodStart; i < chain.size(); i++) {
+                final LALScriptModel.ValueAccessSegment seg = chain.get(i);
+                if (seg instanceof LALScriptModel.MethodSegment) {
+                    current = appendMethodSegment(current,
+                        (LALScriptModel.MethodSegment) seg);
+                } else if (seg instanceof LALScriptModel.IndexSegment) {
+                    current = current + "["
+                        + ((LALScriptModel.IndexSegment) seg).getIndex() + "]";
+                } else if (seg instanceof LALScriptModel.FieldSegment) {
+                    current = current + "."
+                        + ((LALScriptModel.FieldSegment) seg).getName();
+                }
+            }
+        }
+
+        sb.append(current);
+    }
+
+    static String generateExtraLogAccess(
+            final List<LALScriptModel.FieldSegment> fieldSegments,
+            final Class<?> rootType,
+            final String rootExpr,
+            final boolean rootCanBeNull,
+            final LALClassGenerator.GenCtx genCtx) {
+        genCtx.usedProtoAccess = true;
+
+        if (fieldSegments.isEmpty()) {
+            return rootExpr;
+        }
+
+        final String typeName = rootType.getName();
+        final StringBuilder chainKey = new StringBuilder();
+        String prevVar = rootExpr;
+        Class<?> currentType = rootType;
+        boolean prevCanBeNull = rootCanBeNull;
+
+        for (int i = 0; i < fieldSegments.size(); i++) {
+            final LALScriptModel.FieldSegment seg = fieldSegments.get(i);
+            final String field = seg.getName();
+            final String getterName = "get" + Character.toUpperCase(field.charAt(0))
+                + field.substring(1);
+
+            final java.lang.reflect.Method getter;
+            try {
+                getter = currentType.getMethod(getterName);
+            } catch (NoSuchMethodException e) {
+                throw new IllegalArgumentException(
+                    "Cannot resolve getter " + currentType.getSimpleName()
+                        + "." + getterName + "() for type "
+                        + typeName + ". Check the field path in the LAL rule.");
+            }
+            final Class<?> returnType = getter.getReturnType();
+
+            if (chainKey.length() > 0) {
+                chainKey.append(".");
+            }
+            chainKey.append(field);
+            final String key = chainKey.toString();
+            final boolean isLast = i == fieldSegments.size() - 1;
+
+            // Primitive final segment: return inline expression, no variable
+            if (isLast && returnType.isPrimitive()) {
+                final String rawAccess = prevVar + "." + getterName + "()";
+                genCtx.lastResolvedType = returnType;
+                genCtx.lastRawChain = rawAccess;
+                final String boxName = LALCodegenHelper.boxTypeName(returnType);
+                if (seg.isSafeNav() && prevCanBeNull) {
+                    genCtx.lastNullChecks = prevVar + " == null";
+                    return "(" + prevVar + " == null ? null : "
+                        + boxName + ".valueOf(" + rawAccess + "))";
+                } else {
+                    genCtx.lastNullChecks = null;
+                    return boxName + ".valueOf(" + rawAccess + ")";
+                }
+            }
+
+            // Reuse existing variable (dedup)
+            final String existingVar = genCtx.protoVars.get(key);
+            if (existingVar != null) {
+                prevVar = existingVar;
+                currentType = returnType;
+                prevCanBeNull = true;
+                continue;
+            }
+
+            // Create new local variable declaration
+            final String newVar = "_t" + genCtx.protoVarCounter++;
+            final String returnTypeName = returnType.getName();
+            if (seg.isSafeNav() && prevCanBeNull) {
+                genCtx.protoVarDecls.append("  ").append(returnTypeName)
+                    .append(" ").append(newVar).append(" = ")
+                    .append(prevVar).append(" == null ? null : ")
+                    .append(prevVar).append(".").append(getterName).append("();\n");
+                prevCanBeNull = true;
+            } else {
+                genCtx.protoVarDecls.append("  ").append(returnTypeName)
+                    .append(" ").append(newVar).append(" = ")
+                    .append(prevVar).append(".").append(getterName).append("();\n");
+                prevCanBeNull = !returnType.isPrimitive();
+            }
+            genCtx.protoVars.put(key, newVar);
+            genCtx.protoLvtVars.add(new String[]{
+                newVar, "L" + returnTypeName.replace('.', '/') + ";"
+            });
+
+            prevVar = newVar;
+            currentType = returnType;
+        }
+
+        // Non-primitive final result — null checks are in declarations
+        genCtx.lastResolvedType = currentType;
+        genCtx.lastRawChain = prevVar;
+        genCtx.lastNullChecks = null;
+        return prevVar;
+    }
+
+    // ==================== ProcessRegistry ====================
+
+    static void generateProcessRegistryCall(
+            final StringBuilder sb,
+            final LALScriptModel.ValueAccess value,
+            final LALClassGenerator.GenCtx genCtx) {
+        final List<LALScriptModel.ValueAccessSegment> chain = value.getChain();
+        if (chain.isEmpty()) {
+            sb.append("null");
+            return;
+        }
+        final LALScriptModel.ValueAccessSegment seg = chain.get(0);
+        if (seg instanceof LALScriptModel.MethodSegment) {
+            final LALScriptModel.MethodSegment ms =
+                (LALScriptModel.MethodSegment) seg;
+            sb.append(PROCESS_REGISTRY).append(".")
+              .append(ms.getName()).append("(");
+            final List<LALScriptModel.FunctionArg> args = ms.getArguments();
+            for (int i = 0; i < args.size(); i++) {
+                if (i > 0) {
+                    sb.append(", ");
+                }
+                generateCastedValueAccess(sb,
+                    args.get(i).getValue(), args.get(i).getCastType(), genCtx);
+            }
+            sb.append(")");
+        } else {
+            sb.append("null");
+        }
+    }
+
+    // ==================== Utility methods ====================
+
+    static String appendMethodSegment(final String current,
+                                       final LALScriptModel.MethodSegment ms) {
+        if (ms.isSafeNav()) {
+            final String mn = ms.getName();
+            if ("toString".equals(mn)) {
+                return "h.toString(" + current + ")";
+            } else if ("trim".equals(mn)) {
+                return "h.trim(" + current + ")";
+            } else {
+                throw new IllegalArgumentException(
+                    "Unsupported safe-nav method: ?." + mn + "()");
+            }
+        } else {
+            if (ms.getArguments().isEmpty()) {
+                return current + "." + ms.getName() + "()";
+            } else {
+                return current + "." + ms.getName() + "("
+                    + generateMethodArgs(ms.getArguments()) + ")";
+            }
+        }
+    }
+
+    static String generateMethodArgs(
+            final List<LALScriptModel.FunctionArg> args) {
+        final StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < args.size(); i++) {
+            if (i > 0) {
+                sb.append(", ");
+            }
+            final LALScriptModel.FunctionArg arg = args.get(i);
+            final LALScriptModel.ValueAccess va = arg.getValue();
+            if (va.isStringLiteral()) {
+                sb.append("\"").append(LALCodegenHelper.escapeJava(
+                    va.getSegments().get(0))).append("\"");
+            } else if (va.isNumberLiteral()) {
+                sb.append(va.getSegments().get(0));
+            } else {
+                sb.append("null");
+            }
+        }
+        return sb.toString();
+    }
+}
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGenerator.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGenerator.java
new file mode 100644
index 0000000000..bc1c054cfa
--- /dev/null
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGenerator.java
@@ -0,0 +1,553 @@
+/*
+ * 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.v2.compiler;
+
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import javassist.ClassPool;
+import javassist.CtClass;
+import javassist.CtNewConstructor;
+import javassist.CtNewMethod;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.rt.LalExpressionPackageHolder;
+import org.apache.skywalking.oap.log.analyzer.v2.dsl.LalExpression;
+import org.apache.skywalking.oap.server.core.WorkPath;
+import org.apache.skywalking.oap.server.library.util.StringUtil;
+
+/**
+ * Generates {@link LalExpression} implementation classes from
+ * {@link LALScriptModel} AST using Javassist bytecode generation.
+ *
+ * <p>Generates a single class with {@code execute()} and private helper
+ * methods — no consumer classes or callback indirection.
+ *
+ * <p>Block-level code generation (extractor, sink, condition, value access)
+ * is delegated to {@link LALBlockCodegen}. Static utility constants and
+ * methods live in {@link LALCodegenHelper}.
+ */
+@Slf4j
+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.v2.compiler.rt.";
+
+    private static final String FILTER_SPEC =
+        "org.apache.skywalking.oap.log.analyzer.v2.dsl.spec.filter.FilterSpec";
+    private static final String EXEC_CTX =
+        "org.apache.skywalking.oap.log.analyzer.v2.dsl.ExecutionContext";
+    private static final String H =
+        "org.apache.skywalking.oap.log.analyzer.v2.compiler.rt.LalRuntimeHelper";
+
+    private static final java.util.Set<String> USED_CLASS_NAMES =
+        java.util.Collections.synchronizedSet(new java.util.HashSet<>());
+
+    private final ClassPool classPool;
+    private File classOutputDir;
+    private String classNameHint;
+    private Class<?> extraLogType;
+    private String yamlSource;
+
+    // ==================== Parser type detection ====================
+
+    enum ParserType { JSON, YAML, TEXT, NONE }
+
+    static class PrivateMethod {
+        final String source;
+        final String[][] lvtVars;
+
+        PrivateMethod(final String source, final String[][] lvtVars) {
+            this.source = source;
+            this.lvtVars = lvtVars;
+        }
+    }
+
+    static class GenCtx {
+        final ParserType parserType;
+        final Class<?> extraLogType;
+        final List<PrivateMethod> privateMethods = new ArrayList<>();
+        final Map<String, Integer> methodCounts = new HashMap<>();
+
+        // Set by generateExtraLogAccess for primitive optimization in callers.
+        // Reset to null by generateValueAccess at the start of each value access.
+        Class<?> lastResolvedType;
+        String lastNullChecks;
+        String lastRawChain;
+
+        // Per-method proto field variable caching (NONE + extraLogType only).
+        // Maps chain key ("response", "response.responseCode") to variable name ("_t0", "_t1").
+        // Enables dedup: the same chain accessed multiple times reuses the same variable.
+        final Map<String, String> protoVars = new HashMap<>();
+        final List<String[]> protoLvtVars = new ArrayList<>();
+        final StringBuilder protoVarDecls = new StringBuilder();
+        int protoVarCounter;
+        boolean usedProtoAccess;
+
+        GenCtx(final ParserType parserType, final Class<?> extraLogType) {
+            this.parserType = parserType;
+            this.extraLogType = extraLogType;
+        }
+
+        String nextMethodName(final String prefix) {
+            final int count = methodCounts.merge(prefix, 1, Integer::sum);
+            return count == 1 ? "_" + prefix : "_" + prefix + "_" + count;
+        }
+
+        void clearExtraLogResult() {
+            lastResolvedType = null;
+            lastNullChecks = null;
+            lastRawChain = null;
+        }
+
+        void resetProtoVars() {
+            protoVars.clear();
+            protoLvtVars.clear();
+            protoVarDecls.setLength(0);
+            protoVarCounter = 0;
+            usedProtoAccess = false;
+        }
+
+        Object[] saveProtoVarState() {
+            return new Object[]{
+                new HashMap<>(protoVars),
+                new ArrayList<>(protoLvtVars),
+                protoVarDecls.toString(),
+                protoVarCounter,
+                usedProtoAccess
+            };
+        }
+
+        @SuppressWarnings("unchecked")
+        void restoreProtoVarState(final Object[] state) {
+            protoVars.clear();
+            protoVars.putAll((Map<String, String>) state[0]);
+            protoLvtVars.clear();
+            protoLvtVars.addAll((List<String[]>) state[1]);
+            protoVarDecls.setLength(0);
+            protoVarDecls.append((String) state[2]);
+            protoVarCounter = (Integer) state[3];
+            usedProtoAccess = (Boolean) state[4];
+        }
+    }
+
+    public LALClassGenerator() {
+        this(ClassPool.getDefault());
+        if (StringUtil.isNotEmpty(System.getenv("SW_DYNAMIC_CLASS_ENGINE_DEBUG"))) {
+            classOutputDir = new File(WorkPath.getPath().getParentFile(), "lal-rt");
+        }
+    }
+
+    public LALClassGenerator(final ClassPool classPool) {
+        this.classPool = classPool;
+    }
+
+    public void setClassOutputDir(final File dir) {
+        this.classOutputDir = dir;
+    }
+
+    public void setClassNameHint(final String hint) {
+        this.classNameHint = hint;
+    }
+
+    public void setExtraLogType(final Class<?> extraLogType) {
+        this.extraLogType = extraLogType;
+    }
+
+    public void setYamlSource(final String yamlSource) {
+        this.yamlSource = yamlSource;
+    }
+
+    private String makeClassName(final String defaultPrefix) {
+        if (classNameHint != null) {
+            return dedupClassName(
+                PACKAGE_PREFIX + LALCodegenHelper.sanitizeName(classNameHint));
+        }
+        return PACKAGE_PREFIX + defaultPrefix + CLASS_COUNTER.getAndIncrement();
+    }
+
+    private String dedupClassName(final String base) {
+        if (USED_CLASS_NAMES.add(base)) {
+            return base;
+        }
+        for (int i = 2; ; i++) {
+            final String candidate = base + "_" + i;
+            if (USED_CLASS_NAMES.add(candidate)) {
+                return candidate;
+            }
+        }
+    }
+
+    private void writeClassFile(final CtClass ctClass) {
+        if (classOutputDir == null) {
+            return;
+        }
+        if (!classOutputDir.exists()) {
+            classOutputDir.mkdirs();
+        }
+        final File file = new File(classOutputDir, ctClass.getSimpleName() + ".class");
+        try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
+            ctClass.toBytecode(out);
+        } catch (Exception e) {
+            log.warn("Failed to write class file {}: {}", file, e.getMessage());
+        }
+    }
+
+    /**
+     * Adds a {@code LineNumberTable} attribute by scanning bytecode for
+     * store instructions to local variable slots &ge; {@code firstResultSlot}.
+     */
+    private void addLineNumberTable(final javassist.CtMethod method,
+                                    final int firstResultSlot) {
+        try {
+            final javassist.bytecode.MethodInfo mi = method.getMethodInfo();
+            final javassist.bytecode.CodeAttribute code = mi.getCodeAttribute();
+            if (code == null) {
+                return;
+            }
+
+            final List<int[]> entries = new ArrayList<>();
+            int line = 1;
+            boolean nextIsNewLine = true;
+
+            final javassist.bytecode.CodeIterator ci = code.iterator();
+            while (ci.hasNext()) {
+                final int pc = ci.next();
+                if (nextIsNewLine) {
+                    entries.add(new int[]{pc, line++});
+                    nextIsNewLine = false;
+                }
+                final int op = ci.byteAt(pc) & 0xFF;
+                int slot = -1;
+                if (op >= 59 && op <= 78) {
+                    slot = (op - 59) % 4;
+                } else if (op >= 54 && op <= 58) {
+                    slot = ci.byteAt(pc + 1) & 0xFF;
+                }
+                if (slot >= firstResultSlot) {
+                    nextIsNewLine = true;
+                }
+            }
+
+            if (entries.isEmpty()) {
+                return;
+            }
+
+            final javassist.bytecode.ConstPool cp = mi.getConstPool();
+            final byte[] info = new byte[2 + entries.size() * 4];
+            info[0] = (byte) (entries.size() >> 8);
+            info[1] = (byte) entries.size();
+            for (int i = 0; i < entries.size(); i++) {
+                final int off = 2 + i * 4;
+                info[off] = (byte) (entries.get(i)[0] >> 8);
+                info[off + 1] = (byte) entries.get(i)[0];
+                info[off + 2] = (byte) (entries.get(i)[1] >> 8);
+                info[off + 3] = (byte) entries.get(i)[1];
+            }
+            code.getAttributes().add(
+                new javassist.bytecode.AttributeInfo(cp, "LineNumberTable", info));
+        } catch (Exception e) {
+            log.warn("Failed to add LineNumberTable: {}", e.getMessage());
+        }
+    }
+
+    private static void setSourceFile(final CtClass ctClass, final String name) {
+        try {
+            final javassist.bytecode.ClassFile cf = ctClass.getClassFile();
+            final javassist.bytecode.AttributeInfo sf = cf.getAttribute("SourceFile");
+            if (sf != null) {
+                final javassist.bytecode.ConstPool cp = cf.getConstPool();
+                final int idx = cp.addUtf8Info(name);
+                sf.set(new byte[]{(byte) (idx >> 8), (byte) idx});
+            }
+        } catch (Exception e) {
+            // best-effort
+        }
+    }
+
+    /**
+     * Builds the SourceFile name for a generated class. When YAML source info
+     * is available, produces {@code "default(ruleName.java)"};
+     * otherwise falls back to {@code "ruleName.java"}.
+     */
+    private String formatSourceFileName(final String ruleName) {
+        final String classFile = ruleName + ".java";
+        if (yamlSource != null) {
+            return "(" + yamlSource + ")" + classFile;
+        }
+        return classFile;
+    }
+
+    private void addLocalVariableTable(final javassist.CtMethod method,
+                                       final String className,
+                                       final String[][] vars) {
+        try {
+            final javassist.bytecode.MethodInfo mi = method.getMethodInfo();
+            final javassist.bytecode.CodeAttribute code = mi.getCodeAttribute();
+            if (code == null) {
+                return;
+            }
+            final javassist.bytecode.ConstPool cp = mi.getConstPool();
+            final int len = code.getCodeLength();
+            final javassist.bytecode.LocalVariableAttribute lva =
+                new javassist.bytecode.LocalVariableAttribute(cp);
+            lva.addEntry(0, len,
+                cp.addUtf8Info("this"),
+                cp.addUtf8Info("L" + className.replace('.', '/') + ";"), 0);
+            for (int i = 0; i < vars.length; i++) {
+                lva.addEntry(0, len,
+                    cp.addUtf8Info(vars[i][0]),
+                    cp.addUtf8Info(vars[i][1]), i + 1);
+            }
+            code.getAttributes().add(lva);
+        } catch (Exception e) {
+            log.warn("Failed to add LocalVariableTable: {}", e.getMessage());
+        }
+    }
+
+    private static ParserType detectParserType(
+            final List<? extends LALScriptModel.FilterStatement> stmts) {
+        for (final LALScriptModel.FilterStatement stmt : stmts) {
+            if (stmt instanceof LALScriptModel.JsonParser) {
+                return ParserType.JSON;
+            }
+            if (stmt instanceof LALScriptModel.YamlParser) {
+                return ParserType.YAML;
+            }
+            if (stmt instanceof LALScriptModel.TextParser) {
+                return ParserType.TEXT;
+            }
+            if (stmt instanceof LALScriptModel.IfBlock) {
+                final LALScriptModel.IfBlock ifBlock = (LALScriptModel.IfBlock) stmt;
+                ParserType t = detectParserType(ifBlock.getThenBranch());
+                if (t != ParserType.NONE) {
+                    return t;
+                }
+                t = detectParserType(ifBlock.getElseBranch());
+                if (t != ParserType.NONE) {
+                    return t;
+                }
+            }
+        }
+        return ParserType.NONE;
+    }
+
+    // ==================== Compilation ====================
+
+    /**
+     * 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. Generates a single class with
+     * execute() and private helper methods.
+     */
+    public LalExpression compileFromModel(final LALScriptModel model) throws Exception {
+        final String className = makeClassName("LalExpr_");
+        final ParserType parserType = detectParserType(model.getStatements());
+        final GenCtx genCtx = new GenCtx(parserType, this.extraLogType);
+
+        if (parserType == ParserType.NONE && this.extraLogType != null) {
+            log.info("LAL rule has no parser — using extraLogType {} for "
+                + "direct getter calls.", this.extraLogType.getName());
+        }
+
+        final String executeBody = generateExecuteMethod(model, genCtx);
+
+        if (log.isDebugEnabled()) {
+            log.debug("LAL compile AST: {}", model);
+            log.debug("LAL compile execute():\n{}", executeBody);
+            for (final PrivateMethod pm : genCtx.privateMethods) {
+                log.debug("LAL compile private method:\n{}", pm.source);
+            }
+        }
+
+        final CtClass ctClass = classPool.makeClass(className);
+        ctClass.addInterface(classPool.get(
+            "org.apache.skywalking.oap.log.analyzer.v2.dsl.LalExpression"));
+        ctClass.addConstructor(CtNewConstructor.defaultConstructor(ctClass));
+
+        // Add private methods BEFORE execute so Javassist can resolve calls
+        for (final PrivateMethod pm : genCtx.privateMethods) {
+            final javassist.CtMethod ctMethod = CtNewMethod.make(pm.source, ctClass);
+            ctClass.addMethod(ctMethod);
+            addLocalVariableTable(ctMethod, className, pm.lvtVars);
+            addLineNumberTable(ctMethod, pm.lvtVars.length + 1); // after this + params
+        }
+
+        final javassist.CtMethod execMethod = CtNewMethod.make(executeBody, ctClass);
+        ctClass.addMethod(execMethod);
+
+        // Build LVT for execute(): params + h + optional _p and proto vars
+        final List<String[]> execLvt = new ArrayList<>();
+        execLvt.add(new String[]{"filterSpec", "L" + FILTER_SPEC.replace('.', '/') + ";"});
+        execLvt.add(new String[]{"ctx", "L" + EXEC_CTX.replace('.', '/') + ";"});
+        execLvt.add(new String[]{"h", "L" + H.replace('.', '/') + ";"});
+        if (genCtx.usedProtoAccess) {
+            if (genCtx.extraLogType != null) {
+                execLvt.add(new String[]{"_p",
+                    "L" + genCtx.extraLogType.getName().replace('.', '/') + ";"});
+            }
+            execLvt.addAll(genCtx.protoLvtVars);
+        }
+        addLocalVariableTable(execMethod, className,
+            execLvt.toArray(new String[0][]));
+        addLineNumberTable(execMethod, 3); // slot 0=this, 1=filterSpec, 2=ctx
+
+        setSourceFile(ctClass, formatSourceFileName(
+            classNameHint != null ? classNameHint : className));
+
+        writeClassFile(ctClass);
+
+        final Class<?> clazz = ctClass.toClass(LalExpressionPackageHolder.class);
+        ctClass.detach();
+        return (LalExpression) clazz.getDeclaredConstructor().newInstance();
+    }
+
+    private static boolean hasParsedAccess(
+            final List<? extends LALScriptModel.FilterStatement> stmts) {
+        for (final LALScriptModel.FilterStatement stmt : stmts) {
+            if (stmt instanceof LALScriptModel.ExtractorBlock) {
+                return true;
+            }
+            if (stmt instanceof LALScriptModel.IfBlock) {
+                final LALScriptModel.IfBlock ifBlock = (LALScriptModel.IfBlock) stmt;
+                if (hasParsedAccess(ifBlock.getThenBranch())
+                        || hasParsedAccess(ifBlock.getElseBranch())) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    // ==================== Execute method generation ====================
+
+    private String generateExecuteMethod(final LALScriptModel model,
+                                          final GenCtx genCtx) {
+        genCtx.resetProtoVars();
+
+        // Generate body first so proto var declarations are collected
+        final StringBuilder bodyContent = new StringBuilder();
+        for (final LALScriptModel.FilterStatement stmt : model.getStatements()) {
+            generateFilterStatement(bodyContent, stmt, genCtx);
+        }
+
+        final StringBuilder sb = new StringBuilder();
+        sb.append("public void execute(").append(FILTER_SPEC)
+          .append(" filterSpec, ").append(EXEC_CTX).append(" ctx) {\n");
+        sb.append("  ").append(H).append(" h = new ").append(H).append("(ctx);\n");
+
+        // Insert _p + proto var declarations if any proto field access was used
+        if (genCtx.usedProtoAccess) {
+            if (genCtx.extraLogType != null) {
+                final String elTypeName = genCtx.extraLogType.getName();
+                sb.append("  ").append(elTypeName).append(" _p = (")
+                  .append(elTypeName).append(") h.ctx().extraLog();\n");
+            }
+            sb.append(genCtx.protoVarDecls);
+        }
+
+        sb.append(bodyContent);
+        sb.append("}\n");
+        return sb.toString();
+    }
+
+    private void generateFilterStatement(final StringBuilder sb,
+                                          final LALScriptModel.FilterStatement stmt,
+                                          final GenCtx genCtx) {
+        if (stmt instanceof LALScriptModel.TextParser) {
+            final LALScriptModel.TextParser tp = (LALScriptModel.TextParser) stmt;
+            if (tp.getRegexpPattern() != null) {
+                sb.append("  filterSpec.textWithRegexp(ctx, \"")
+                  .append(LALCodegenHelper.escapeJava(tp.getRegexpPattern()))
+                  .append("\");\n");
+            } else {
+                sb.append("  filterSpec.text(ctx);\n");
+            }
+        } else if (stmt instanceof LALScriptModel.JsonParser) {
+            sb.append("  filterSpec.json(ctx);\n");
+        } else if (stmt instanceof LALScriptModel.YamlParser) {
+            sb.append("  filterSpec.yaml(ctx);\n");
+        } else if (stmt instanceof LALScriptModel.AbortStatement) {
+            sb.append("  filterSpec.abort(ctx);\n");
+        } else if (stmt instanceof LALScriptModel.ExtractorBlock) {
+            LALBlockCodegen.generateExtractorMethod(
+                sb, (LALScriptModel.ExtractorBlock) stmt, genCtx);
+        } else if (stmt instanceof LALScriptModel.SinkBlock) {
+            final LALScriptModel.SinkBlock sink = (LALScriptModel.SinkBlock) stmt;
+            if (sink.getStatements().isEmpty()) {
+                sb.append("  filterSpec.sink(ctx);\n");
+            } else {
+                LALBlockCodegen.generateSinkMethod(sb, sink, genCtx);
+            }
+        } else if (stmt instanceof LALScriptModel.IfBlock) {
+            generateTopLevelIfBlock(sb, (LALScriptModel.IfBlock) stmt, genCtx);
+        }
+    }
+
+    private void generateTopLevelIfBlock(final StringBuilder sb,
+                                          final LALScriptModel.IfBlock ifBlock,
+                                          final GenCtx genCtx) {
+        sb.append("  if (");
+        LALBlockCodegen.generateCondition(sb, ifBlock.getCondition(), genCtx);
+        sb.append(") {\n");
+        for (final LALScriptModel.FilterStatement s : ifBlock.getThenBranch()) {
+            generateFilterStatement(sb, s, genCtx);
+        }
+        sb.append("  }\n");
+        if (!ifBlock.getElseBranch().isEmpty()) {
+            sb.append("  else {\n");
+            for (final LALScriptModel.FilterStatement s : ifBlock.getElseBranch()) {
+                generateFilterStatement(sb, s, genCtx);
+            }
+            sb.append("  }\n");
+        }
+    }
+
+    // ==================== Source generation (for testing) ====================
+
+    /**
+     * Generates the Java source of execute() + private methods for
+     * debugging/testing.
+     */
+    public String generateSource(final String dsl) {
+        final LALScriptModel model = LALScriptParser.parse(dsl);
+        final GenCtx genCtx = new GenCtx(
+            detectParserType(model.getStatements()), this.extraLogType);
+        final String execute = generateExecuteMethod(model, genCtx);
+        if (genCtx.privateMethods.isEmpty()) {
+            return execute;
+        }
+        final StringBuilder all = new StringBuilder(execute);
+        for (final PrivateMethod m : genCtx.privateMethods) {
+            all.append("\n").append(m.source);
+        }
+        return all.toString();
+    }
+}
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALCodegenHelper.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALCodegenHelper.java
new file mode 100644
index 0000000000..0947ee263c
--- /dev/null
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALCodegenHelper.java
@@ -0,0 +1,107 @@
+/*
+ * 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.v2.compiler;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Static utility constants and methods extracted from {@link LALClassGenerator}
+ * for reuse by {@link LALBlockCodegen}.
+ */
+final class LALCodegenHelper {
+
+    static final Map<String, String> LOG_GETTERS = new HashMap<>();
+    static final Map<String, String> TRACE_CONTEXT_GETTERS = new HashMap<>();
+    static final Set<String> LONG_FIELDS = new HashSet<>();
+    static final Set<String> INT_FIELDS = new HashSet<>();
+
+    static {
+        LOG_GETTERS.put("service", "getService");
+        LOG_GETTERS.put("serviceInstance", "getServiceInstance");
+        LOG_GETTERS.put("endpoint", "getEndpoint");
+        LOG_GETTERS.put("timestamp", "getTimestamp");
+        LOG_GETTERS.put("body", "getBody");
+        LOG_GETTERS.put("traceContext", "getTraceContext");
+        LOG_GETTERS.put("tags", "getTags");
+        LOG_GETTERS.put("layer", "getLayer");
+
+        TRACE_CONTEXT_GETTERS.put("traceId", "getTraceId");
+        TRACE_CONTEXT_GETTERS.put("traceSegmentId", "getTraceSegmentId");
+        TRACE_CONTEXT_GETTERS.put("spanId", "getSpanId");
+
+        LONG_FIELDS.add("timestamp");
+        INT_FIELDS.add("spanId");
+    }
+
+    private LALCodegenHelper() {
+        // utility class
+    }
+
+    static String escapeJava(final String s) {
+        return s.replace("\\", "\\\\")
+                .replace("\"", "\\\"")
+                .replace("\n", "\\n")
+                .replace("\r", "\\r")
+                .replace("\t", "\\t");
+    }
+
+    static String boxTypeName(final Class<?> primitiveType) {
+        if (primitiveType == int.class) {
+            return "Integer";
+        } else if (primitiveType == long.class) {
+            return "Long";
+        } else if (primitiveType == boolean.class) {
+            return "Boolean";
+        } else if (primitiveType == double.class) {
+            return "Double";
+        } else if (primitiveType == float.class) {
+            return "Float";
+        }
+        return null;
+    }
+
+    static String sanitizeName(final String name) {
+        final StringBuilder sb = new StringBuilder(name.length());
+        for (int i = 0; i < name.length(); i++) {
+            final char c = name.charAt(i);
+            sb.append(i == 0
+                ? (Character.isJavaIdentifierStart(c) ? c : '_')
+                : (Character.isJavaIdentifierPart(c) ? c : '_'));
+        }
+        return sb.length() == 0 ? "Generated" : sb.toString();
+    }
+
+    static String generateMapValCall(final List<String> keys) {
+        if (keys.isEmpty()) {
+            return "h.ctx().parsed()";
+        }
+        final StringBuilder call = new StringBuilder("h.mapVal(");
+        for (int i = 0; i < keys.size(); i++) {
+            if (i > 0) {
+                call.append(", ");
+            }
+            call.append("\"").append(escapeJava(keys.get(i))).append("\"");
+        }
+        call.append(")");
+        return call.toString();
+    }
+}
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALScriptModel.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALScriptModel.java
new file mode 100644
index 0000000000..762e9cbd0b
--- /dev/null
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALScriptModel.java
@@ -0,0 +1,563 @@
+/*
+ * 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.v2.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, FilterStatement {
+        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 List<InterpolationPart> idParts;
+        private final long rpm;
+
+        public RateLimitBlock(final String id,
+                              final List<InterpolationPart> idParts,
+                              final long rpm) {
+            this.id = id;
+            this.idParts = idParts != null
+                ? Collections.unmodifiableList(idParts) : Collections.emptyList();
+            this.rpm = rpm;
+        }
+
+        public boolean isIdInterpolated() {
+            return !idParts.isEmpty();
+        }
+    }
+
+    @Getter
+    public static final class InterpolationPart {
+        private final String literal;
+        private final ValueAccess expression;
+
+        private InterpolationPart(final String literal, final ValueAccess expression) {
+            this.literal = literal;
+            this.expression = expression;
+        }
+
+        public static InterpolationPart ofLiteral(final String text) {
+            return new InterpolationPart(text, null);
+        }
+
+        public static InterpolationPart ofExpression(final ValueAccess expr) {
+            return new InterpolationPart(null, expr);
+        }
+
+        public boolean isLiteral() {
+            return literal != null;
+        }
+    }
+
+    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 boolean processRegistryRef;
+        private final boolean stringLiteral;
+        private final boolean numberLiteral;
+        private final List<ValueAccessSegment> chain;
+        private final String functionCallName;
+        private final List<FunctionArg> functionCallArgs;
+        private final List<ValueAccess> concatParts;
+        private final ValueAccess parenInner;
+        private final String parenCast;
+
+        public ValueAccess(final List<String> segments,
+                           final boolean parsedRef,
+                           final boolean logRef,
+                           final List<ValueAccessSegment> chain) {
+            this(segments, parsedRef, logRef, false, false, false,
+                chain, null, Collections.emptyList(),
+                Collections.emptyList(), null, null);
+        }
+
+        public ValueAccess(final List<String> segments,
+                           final boolean parsedRef,
+                           final boolean logRef,
+                           final boolean processRegistryRef,
+                           final boolean stringLiteral,
+                           final boolean numberLiteral,
+                           final List<ValueAccessSegment> chain,
+                           final String functionCallName,
+                           final List<FunctionArg> functionCallArgs) {
+            this(segments, parsedRef, logRef, processRegistryRef,
+                stringLiteral, numberLiteral, chain,
+                functionCallName, functionCallArgs,
+                Collections.emptyList(), null, null);
+        }
+
+        public ValueAccess(final List<String> segments,
+                           final boolean parsedRef,
+                           final boolean logRef,
+                           final boolean processRegistryRef,
+                           final boolean stringLiteral,
+                           final boolean numberLiteral,
+                           final List<ValueAccessSegment> chain,
+                           final String functionCallName,
+                           final List<FunctionArg> functionCallArgs,
+                           final List<ValueAccess> concatParts,
+                           final ValueAccess parenInner,
+                           final String parenCast) {
+            this.segments = Collections.unmodifiableList(segments);
+            this.parsedRef = parsedRef;
+            this.logRef = logRef;
+            this.processRegistryRef = processRegistryRef;
+            this.stringLiteral = stringLiteral;
+            this.numberLiteral = numberLiteral;
+            this.chain = chain != null
+                ? Collections.unmodifiableList(chain) : Collections.emptyList();
+            this.functionCallName = functionCallName;
+            this.functionCallArgs = functionCallArgs != null
+                ? Collections.unmodifiableList(functionCallArgs) : Collections.emptyList();
+            this.concatParts = concatParts != null
+                ? Collections.unmodifiableList(concatParts) : Collections.emptyList();
+            this.parenInner = parenInner;
+            this.parenCast = parenCast;
+        }
+
+        public String toPathString() {
+            return String.join(".", segments);
+        }
+    }
+
+    @Getter
+    public static final class FunctionArg {
+        private final ValueAccess value;
+        private final String castType;
+
+        public FunctionArg(final ValueAccess value, final String castType) {
+            this.value = value;
+            this.castType = castType;
+        }
+    }
+
+    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<FunctionArg> arguments;
+        private final boolean safeNav;
+
+        public MethodSegment(final String name, final List<FunctionArg> arguments,
+                             final boolean safeNav) {
+            this.name = name;
+            this.arguments = arguments != null
+                ? Collections.unmodifiableList(arguments) : Collections.emptyList();
+            this.safeNav = safeNav;
+        }
+    }
+
+    @Getter
+    public static final class IndexSegment implements ValueAccessSegment {
+        private final int index;
+
+        public IndexSegment(final int index) {
+            this.index = index;
+        }
+    }
+
+    // ==================== 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/v2/compiler/LALScriptParser.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALScriptParser.java
new file mode 100644
index 0000000000..55644c7fe5
--- /dev/null
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALScriptParser.java
@@ -0,0 +1,947 @@
+/*
+ * 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.v2.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.v2.compiler.LALScriptModel.AbortStatement;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.InterpolationPart;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.CompareOp;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.ComparisonCondition;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.Condition;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.DropperStatement;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.EnforcerStatement;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.ExprCondition;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.ExtractorBlock;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.ExtractorStatement;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.FieldAssignment;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.FieldSegment;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.FieldType;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.FilterStatement;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.IfBlock;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.JsonParser;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.LogicalCondition;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.LogicalOp;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.MetricsBlock;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.NotCondition;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.NullConditionValue;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.NumberConditionValue;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.RateLimitBlock;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.SamplerBlock;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.SamplerContent;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.SinkBlock;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.SinkStatement;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.SlowSqlBlock;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.StringConditionValue;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.TagAssignment;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.TagValue;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.TextParser;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.ValueAccess;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.ValueAccessConditionValue;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.ValueAccessSegment;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.IndexSegment;
+import org.apache.skywalking.oap.log.analyzer.v2.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());
+            final List<InterpolationPart> idParts = parseInterpolation(id);
+            contents.add(new RateLimitBlock(id, idParts, 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 int condCount = ctx.condition().size();
+        final int bodyCount = ctx.ifBody().size();
+        // Whether there is a trailing else (no condition) block
+        final boolean hasElse = bodyCount > condCount;
+
+        // Build the chain from the last else-if backwards.
+        // For: if(A){b0} else if(B){b1} else if(C){b2} else{b3}
+        //   condCount=3, bodyCount=4, hasElse=true
+        //   Result: IfBlock(A, b0, IfBlock(B, b1, IfBlock(C, b2, b3)))
+
+        // Start from the innermost else-if (last condition)
+        List<FilterStatement> trailingElse = hasElse
+            ? visitIfBody(ctx.ifBody(bodyCount - 1)) : null;
+
+        // Build from the last condition backwards to index 1
+        IfBlock nested = null;
+        for (int i = condCount - 1; i >= 1; i--) {
+            final Condition cond = visitCondition(ctx.condition(i));
+            final List<FilterStatement> body = visitIfBody(ctx.ifBody(i));
+            final List<FilterStatement> elsePart;
+            if (nested != null) {
+                elsePart = List.of(nested);
+            } else {
+                elsePart = trailingElse;
+            }
+            nested = new IfBlock(cond, body, elsePart);
+        }
+
+        // Build the outermost if block (index 0)
+        final Condition topCond = visitCondition(ctx.condition(0));
+        final List<FilterStatement> topBody = visitIfBody(ctx.ifBody(0));
+        final List<FilterStatement> topElse;
+        if (nested != null) {
+            topElse = List.of(nested);
+        } else {
+            topElse = trailingElse;
+        }
+
+        return new IfBlock(topCond, topBody, topElse);
+    }
+
+    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());
+            }
+        }
+        for (final LALParser.SampledTraceStatementContext stc :
+                ctx.sampledTraceStatement()) {
+            if (stc.ifStatement() != null) {
+                stmts.add((FilterStatement) visitIfStatement(stc.ifStatement()));
+            } else {
+                stmts.add((FilterStatement) visitSampledTraceField(stc));
+            }
+        }
+        // Handle samplerContent alternative (rateLimit blocks inside if within sampler)
+        final LALParser.SamplerContentContext sc = ctx.samplerContent();
+        if (sc != null) {
+            final List<SamplerContent> samplerItems = new ArrayList<>();
+            for (final LALParser.RateLimitBlockContext rlc : sc.rateLimitBlock()) {
+                final String id = stripQuotes(rlc.rateLimitId().getText());
+                final long rpm = Long.parseLong(
+                    rlc.rateLimitContent().NUMBER().getText());
+                final List<InterpolationPart> idParts = parseInterpolation(id);
+                samplerItems.add(new RateLimitBlock(id, idParts, rpm));
+            }
+            for (final LALParser.IfStatementContext isc : sc.ifStatement()) {
+                samplerItems.add((SamplerContent) visitIfStatement(isc));
+            }
+            if (!samplerItems.isEmpty()) {
+                stmts.add((FilterStatement) new SamplerBlock(samplerItems));
+            }
+        }
+        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.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));
+        }
+        if (leftCtx instanceof LALParser.CondFunctionCallContext) {
+            final LALParser.FunctionInvocationContext fi =
+                ((LALParser.CondFunctionCallContext) leftCtx).functionInvocation();
+            final String funcName = fi.functionName().getText();
+            final List<LALScriptModel.FunctionArg> funcArgs = visitFunctionArgs(fi);
+            final ValueAccess left = new ValueAccess(
+                List.of(fi.getText()), false, false, false, false, false,
+                List.of(), funcName, funcArgs);
+            return new ComparisonCondition(left, null, op,
+                visitConditionExprAsValue(rightCtx));
+        }
+        // For 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;
+            // ANTLR grammar routes NUMBER/NULL/STRING/bool through condValueAccess
+            // (since valueAccessPrimary includes them and condValueAccess has priority).
+            // Detect standalone literals and create proper ConditionValue types.
+            final LALParser.ValueAccessContext vaCtx = va.valueAccess();
+            if (va.typeCast() == null && vaCtx.valueAccessTerm().size() == 1
+                    && vaCtx.valueAccessTerm(0).valueAccessSegment().isEmpty()) {
+                final LALParser.ValueAccessPrimaryContext primary =
+                    vaCtx.valueAccessTerm(0).valueAccessPrimary();
+                if (primary instanceof LALParser.ValueNumberContext) {
+                    return new NumberConditionValue(Double.parseDouble(
+                        ((LALParser.ValueNumberContext) primary).NUMBER().getText()));
+                }
+                if (primary instanceof LALParser.ValueNullContext) {
+                    return new NullConditionValue();
+                }
+            }
+            final String cast = va.typeCast() != null ? extractCastType(va.typeCast()) : null;
+            return new ValueAccessConditionValue(visitValueAccess(vaCtx), cast);
+        }
+        if (ctx instanceof LALParser.CondParenGroupContext) {
+            // (condition) used as a value — e.g. in: if ((x == y)) { ... }
+            // Wrap as a ValueAccess containing the paren expression text
+            return new ValueAccessConditionValue(
+                new ValueAccess(List.of(ctx.getText()), false, false, List.of()), null);
+        }
+        // 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);
+        }
+        if (ctx instanceof LALParser.CondFunctionCallContext) {
+            final LALParser.FunctionInvocationContext fi =
+                ((LALParser.CondFunctionCallContext) ctx).functionInvocation();
+            final String funcName = fi.functionName().getText();
+            final List<LALScriptModel.FunctionArg> funcArgs = visitFunctionArgs(fi);
+            final ValueAccess va = new ValueAccess(
+                List.of(fi.getText()), false, false, false, false, false,
+                List.of(), funcName, funcArgs);
+            return new ExprCondition(va, null);
+        }
+        if (ctx instanceof LALParser.CondParenGroupContext) {
+            return visitCondition(
+                ((LALParser.CondParenGroupContext) ctx).condition());
+        }
+        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<LALParser.ValueAccessTermContext> terms = ctx.valueAccessTerm();
+        if (terms.size() == 1) {
+            return visitValueAccessTerm(terms.get(0));
+        }
+        // Multiple terms joined by PLUS — string concatenation
+        final List<ValueAccess> parts = new ArrayList<>();
+        for (final LALParser.ValueAccessTermContext term : terms) {
+            parts.add(visitValueAccessTerm(term));
+        }
+        return new ValueAccess(
+            List.of("concat"), false, false, false, false, false,
+            List.of(), null, null,
+            parts, null, null);
+    }
+
+    private static ValueAccess visitValueAccessTerm(
+            final LALParser.ValueAccessTermContext ctx) {
+        final List<String> segments = new ArrayList<>();
+        boolean parsedRef = false;
+        boolean logRef = false;
+        boolean processRegistryRef = false;
+        boolean stringLiteral = false;
+        boolean numberLiteral = false;
+        String functionCallName = null;
+        List<LALScriptModel.FunctionArg> functionCallArgs = null;
+        ValueAccess parenInner = null;
+        String parenCast = null;
+
+        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.ValueProcessRegistryContext) {
+            processRegistryRef = true;
+            segments.add("ProcessRegistry");
+        } else if (primary instanceof LALParser.ValueIdentifierContext) {
+            segments.add(((LALParser.ValueIdentifierContext) primary).IDENTIFIER().getText());
+        } else if (primary instanceof LALParser.ValueStringContext) {
+            stringLiteral = true;
+            segments.add(stripQuotes(
+                ((LALParser.ValueStringContext) primary).STRING().getText()));
+        } else if (primary instanceof LALParser.ValueNumberContext) {
+            numberLiteral = true;
+            segments.add(((LALParser.ValueNumberContext) primary).NUMBER().getText());
+        } else if (primary instanceof LALParser.ValueFunctionCallContext) {
+            final LALParser.FunctionInvocationContext fi =
+                ((LALParser.ValueFunctionCallContext) primary).functionInvocation();
+            functionCallName = fi.functionName().getText();
+            functionCallArgs = visitFunctionArgs(fi);
+            segments.add(fi.getText());
+        } else if (primary instanceof LALParser.ValueParenContext) {
+            final LALParser.ValueParenContext parenCtx =
+                (LALParser.ValueParenContext) primary;
+            parenInner = visitValueAccess(parenCtx.valueAccess());
+            parenCast = parenCtx.typeCast() != null
+                ? extractCastType(parenCtx.typeCast()) : null;
+            segments.add("paren");
+        } 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.functionName().getText() + "()");
+                chain.add(new LALScriptModel.MethodSegment(
+                    fi.functionName().getText(), visitFunctionArgs(fi), false));
+            } else if (seg instanceof LALParser.SegmentSafeMethodContext) {
+                final LALParser.FunctionInvocationContext fi =
+                    ((LALParser.SegmentSafeMethodContext) seg).functionInvocation();
+                segments.add(fi.functionName().getText() + "()");
+                chain.add(new LALScriptModel.MethodSegment(
+                    fi.functionName().getText(), visitFunctionArgs(fi), true));
+            } else if (seg instanceof LALParser.SegmentIndexContext) {
+                final int index = Integer.parseInt(
+                    ((LALParser.SegmentIndexContext) seg).NUMBER().getText());
+                segments.add("[" + index + "]");
+                chain.add(new IndexSegment(index));
+            }
+        }
+
+        return new ValueAccess(segments, parsedRef, logRef,
+            processRegistryRef, stringLiteral, numberLiteral,
+            chain, functionCallName, functionCallArgs,
+            List.of(), parenInner, parenCast);
+    }
+
+    private static List<LALScriptModel.FunctionArg> visitFunctionArgs(
+            final LALParser.FunctionInvocationContext fi) {
+        if (fi.functionArgList() == null) {
+            return List.of();
+        }
+        final List<LALScriptModel.FunctionArg> args = new ArrayList<>();
+        for (final LALParser.FunctionArgContext fac : fi.functionArgList().functionArg()) {
+            if (fac.valueAccess() != null) {
+                final ValueAccess va = visitValueAccess(fac.valueAccess());
+                final String cast = fac.typeCast() != null
+                    ? extractCastType(fac.typeCast()) : null;
+                args.add(new LALScriptModel.FunctionArg(va, cast));
+            } else if (fac.STRING() != null) {
+                final String val = stripQuotes(fac.STRING().getText());
+                final ValueAccess va = new ValueAccess(
+                    List.of(val), false, false, true, true, false,
+                    List.of(), null, null);
+                args.add(new LALScriptModel.FunctionArg(va, null));
+            } else if (fac.NUMBER() != null) {
+                final ValueAccess va = new ValueAccess(
+                    List.of(fac.NUMBER().getText()), false, false,
+                    false, false, true, List.of(), null, null);
+                args.add(new LALScriptModel.FunctionArg(va, null));
+            } else if (fac.boolValue() != null) {
+                final ValueAccess va = new ValueAccess(
+                    List.of(fac.boolValue().getText()), false, false,
+                    false, false, false, List.of(), null, null);
+                args.add(new LALScriptModel.FunctionArg(va, null));
+            } else {
+                // NULL
+                final ValueAccess va = new ValueAccess(
+                    List.of("null"), false, false, List.of());
+                args.add(new LALScriptModel.FunctionArg(va, null));
+            }
+        }
+        return args;
+    }
+
+    private static String resolveValueAsString(final LALParser.ValueAccessContext ctx) {
+        final LALParser.ValueAccessPrimaryContext primary =
+            ctx.valueAccessTerm(0).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) + "...";
+    }
+
+    // ==================== GString interpolation ====================
+
+    /**
+     * Parses Groovy-style GString interpolation in a string.
+     * E.g. {@code "${log.service}:${parsed?.field}"} produces
+     * [expr(log.service), literal(":"), expr(parsed?.field)].
+     *
+     * @return list of parts, or {@code null} if no interpolation found
+     */
+    static List<InterpolationPart> parseInterpolation(final String s) {
+        if (!s.contains("${")) {
+            return null;
+        }
+        final List<InterpolationPart> parts = new ArrayList<>();
+        int pos = 0;
+        while (pos < s.length()) {
+            final int start = s.indexOf("${", pos);
+            if (start < 0) {
+                // Remaining literal text
+                if (pos < s.length()) {
+                    parts.add(InterpolationPart.ofLiteral(s.substring(pos)));
+                }
+                break;
+            }
+            // Literal text before ${
+            if (start > pos) {
+                parts.add(InterpolationPart.ofLiteral(s.substring(pos, start)));
+            }
+            // Find matching closing brace, respecting nesting
+            int depth = 1;
+            int i = start + 2;
+            while (i < s.length() && depth > 0) {
+                final char c = s.charAt(i);
+                if (c == '{') {
+                    depth++;
+                } else if (c == '}') {
+                    depth--;
+                }
+                i++;
+            }
+            if (depth != 0) {
+                throw new IllegalArgumentException(
+                    "Unclosed interpolation in: " + s);
+            }
+            final String expr = s.substring(start + 2, i - 1);
+            // Parse the expression as a valueAccess through ANTLR
+            parts.add(InterpolationPart.ofExpression(parseValueAccessExpr(expr)));
+            pos = i;
+        }
+        return parts;
+    }
+
+    /**
+     * Parses a standalone valueAccess expression string by wrapping it in
+     * a minimal LAL script and extracting the parsed ValueAccess.
+     */
+    private static ValueAccess parseValueAccessExpr(final String expr) {
+        // Wrap in: filter { if (EXPR) { sink {} } }
+        // The expression becomes a condition, parsed as ExprCondition
+        // whose ValueAccess is what we want.
+        final String wrapper = "filter { if (" + expr + ") { sink {} } }";
+        final LALScriptModel model = parse(wrapper);
+        final IfBlock ifBlock = (IfBlock) model.getStatements().get(0);
+        final LALScriptModel.Condition cond = ifBlock.getCondition();
+        if (cond instanceof ExprCondition) {
+            return ((ExprCondition) cond).getExpr();
+        }
+        if (cond instanceof ComparisonCondition) {
+            return ((ComparisonCondition) cond).getLeft();
+        }
+        throw new IllegalArgumentException(
+            "Cannot parse interpolation expression: " + expr);
+    }
+}
diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/DownsamplingType.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/rt/LalExpressionPackageHolder.java
similarity index 74%
copy from oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/DownsamplingType.java
copy to oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/rt/LalExpressionPackageHolder.java
index c8f0006fbb..ef74d9cab1 100644
--- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/DownsamplingType.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/rt/LalExpressionPackageHolder.java
@@ -13,14 +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;
+package org.apache.skywalking.oap.log.analyzer.v2.compiler.rt;
 
 /**
- * DownsamplingType indicates the downsampling type of meter function
+ * 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.
  */
-public enum DownsamplingType {
-    AVG, SUM, LATEST, SUM_PER_MIN, MAX, MIN
+public class LalExpressionPackageHolder {
 }
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/rt/LalRuntimeHelper.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/rt/LalRuntimeHelper.java
new file mode 100644
index 0000000000..f9e22118e3
--- /dev/null
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/rt/LalRuntimeHelper.java
@@ -0,0 +1,316 @@
+/*
+ * 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.v2.compiler.rt;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.skywalking.apm.network.common.v3.KeyStringValuePair;
+import org.apache.skywalking.oap.log.analyzer.v2.dsl.ExecutionContext;
+
+/**
+ * Runtime helper for compiled LAL expressions.
+ *
+ * <p>Created once per {@code execute()} call, holds the {@link ExecutionContext}
+ * and provides data-source-specific access and type conversion methods.
+ *
+ * <h2>Data Source Methods</h2>
+ *
+ * <p><b>1. JSON/YAML map data</b> — used when the LAL script has a {@code json {}} or
+ * {@code yaml {}} parser. The parsed body is stored in {@code ctx.parsed().getMap()}.
+ * <pre>
+ *   // LAL:  parsed.service as String
+ *   // Generated:  h.toStr(h.mapVal("service"))
+ *
+ *   // LAL:  parsed.client_process.address as String  (nested map)
+ *   // Generated:  h.toStr(h.mapVal("client_process", "address"))
+ * </pre>
+ *
+ * <p><b>2. Text regexp matcher data</b> — used when the LAL script has a
+ * {@code text { regexp '...' }} parser. The parsed body is stored as a
+ * {@link java.util.regex.Matcher} in {@code ctx.parsed().getMatcher()}.
+ * <pre>
+ *   // LAL:  parsed.level as String
+ *   // Generated:  h.toStr(h.group("level"))
+ * </pre>
+ *
+ * <p><b>3. Tag data</b> — accesses log tags (protobuf {@code KeyStringValuePair} list).
+ * Available regardless of parser type.
+ * <pre>
+ *   // LAL:  tag("LOG_KIND")
+ *   // Generated:  h.tagValue("LOG_KIND")
+ * </pre>
+ *
+ * <p><b>4. Log proto data</b> — direct access to {@code LogData.Builder} fields.
+ * Not accessed through this helper; the compiler generates direct getter chains
+ * like {@code h.ctx().log().getService()}.
+ *
+ * <p><b>5. ExtraLog proto data</b> — direct access to typed protobuf extraLog.
+ * Not accessed through this helper; the compiler generates typed cast + getter
+ * chains like {@code ((HTTPAccessLogEntry) h.ctx().extraLog()).getResponse()}.
+ *
+ * <h2>Type Conversion Methods</h2>
+ *
+ * <p>Convert parsed values (typically {@code Object}) to typed values for
+ * spec method calls.
+ * <pre>
+ *   // LAL:  parsed.service as String    →  h.toStr(h.mapVal("service"))
+ *   // LAL:  parsed.latency as Long      →  Long.valueOf(h.toLong(h.mapVal("latency")))
+ *   // LAL:  parsed.ssl as Boolean       →  Boolean.valueOf(h.toBool(h.mapVal("ssl")))
+ *   // LAL:  parsed.code as Integer      →  Integer.valueOf(h.toInt(h.mapVal("code")))
+ * </pre>
+ *
+ * <h2>Safe Navigation Methods</h2>
+ * <pre>
+ *   // LAL:  parsed?.x?.toString()       →  h.toString(h.mapVal("x"))
+ *   // LAL:  parsed?.x?.trim()           →  h.trim(h.mapVal("x"))
+ * </pre>
+ *
+ * <h2>Boolean Evaluation Methods</h2>
+ * <pre>
+ *   // LAL if-condition:  if (parsed.flag)        →  h.isTrue(h.mapVal("flag"))
+ *   // LAL if-condition:  if (parsed.name)        →  h.isNotEmpty(h.mapVal("name"))
+ * </pre>
+ */
+public final class LalRuntimeHelper {
+
+    private final ExecutionContext ctx;
+
+    public LalRuntimeHelper(final ExecutionContext ctx) {
+        this.ctx = ctx;
+    }
+
+    public ExecutionContext ctx() {
+        return ctx;
+    }
+
+    // ==================== Data source: JSON/YAML map ====================
+    // Used when LAL has json{} or yaml{} parser.
+    // Returns raw Object from the parsed Map<String,Object>.
+
+    /**
+     * Single-key map access.
+     * <pre>
+     *   // LAL:  parsed.service    →  h.mapVal("service")
+     * </pre>
+     */
+    public Object mapVal(final String key) {
+        return ctx.parsed().getMap().get(key);
+    }
+
+    /**
+     * Two-level nested map access.
+     * <pre>
+     *   // LAL:  parsed.a.b    →  h.mapVal("a", "b")
+     * </pre>
+     */
+    public Object mapVal(final String k1, final String k2) {
+        return mapGet(mapVal(k1), k2);
+    }
+
+    /**
+     * Three-level nested map access.
+     * <pre>
+     *   // LAL:  parsed.a.b.c    →  h.mapVal("a", "b", "c")
+     * </pre>
+     */
+    public Object mapVal(final String k1, final String k2, final String k3) {
+        return mapGet(mapVal(k1, k2), k3);
+    }
+
+    private static Object mapGet(final Object obj, final String key) {
+        if (obj == null) {
+            return null;
+        }
+        if (obj instanceof Map) {
+            return ((Map) obj).get(key);
+        }
+        return null;
+    }
+
+    // ==================== Data source: Text regexp matcher ====================
+    // Used when LAL has text { regexp '...' } parser.
+    // Returns String from named matcher group.
+
+    /**
+     * Named matcher group access.
+     * <pre>
+     *   // LAL:  parsed.level    →  h.group("level")
+     * </pre>
+     */
+    public String group(final String name) {
+        return ctx.parsed().getMatcher().group(name);
+    }
+
+    // ==================== Data source: Log tags ====================
+    // Available for all LAL scripts.
+
+    /**
+     * Log tag lookup by key name.
+     * <pre>
+     *   // LAL:  tag("LOG_KIND")    →  h.tagValue("LOG_KIND")
+     * </pre>
+     */
+    public String tagValue(final String key) {
+        final List dl = ctx.log().getTags().getDataList();
+        for (int i = 0; i < dl.size(); i++) {
+            final KeyStringValuePair kv = (KeyStringValuePair) dl.get(i);
+            if (key.equals(kv.getKey())) {
+                return kv.getValue();
+            }
+        }
+        return "";
+    }
+
+    // ==================== Type conversion ====================
+
+    /**
+     * {@code as String} cast — null-safe, returns null for null input.
+     * <pre>
+     *   // LAL:  parsed.service as String    →  h.toStr(h.mapVal("service"))
+     * </pre>
+     */
+    public String toStr(final Object obj) {
+        return obj == null ? null : String.valueOf(obj);
+    }
+
+    /**
+     * {@code as Long} cast — Number or String to long.
+     * <pre>
+     *   // LAL:  parsed.latency as Long    →  Long.valueOf(h.toLong(h.mapVal("latency")))
+     * </pre>
+     */
+    public long toLong(final Object obj) {
+        if (obj instanceof Number) {
+            return ((Number) obj).longValue();
+        }
+        if (obj instanceof String) {
+            return Long.parseLong((String) obj);
+        }
+        return 0L;
+    }
+
+    /**
+     * {@code as Integer} cast — Number or String to int.
+     * <pre>
+     *   // LAL:  parsed.code as Integer    →  Integer.valueOf(h.toInt(h.mapVal("code")))
+     * </pre>
+     */
+    public int toInt(final Object obj) {
+        if (obj instanceof Number) {
+            return ((Number) obj).intValue();
+        }
+        if (obj instanceof String) {
+            return Integer.parseInt((String) obj);
+        }
+        return 0;
+    }
+
+    /**
+     * {@code as Boolean} cast — Boolean, String, or non-null to boolean.
+     * <pre>
+     *   // LAL:  parsed.ssl as Boolean    →  Boolean.valueOf(h.toBool(h.mapVal("ssl")))
+     * </pre>
+     */
+    public boolean toBool(final Object obj) {
+        if (obj instanceof Boolean) {
+            return ((Boolean) obj).booleanValue();
+        }
+        if (obj instanceof String) {
+            return Boolean.parseBoolean((String) obj);
+        }
+        return obj != null;
+    }
+
+    // ==================== Boolean evaluation ====================
+
+    /**
+     * Boolean truthiness for if-conditions: null is false, Boolean delegates,
+     * String parses, anything else is true.
+     * <pre>
+     *   // LAL:  if (parsed.flag)    →  h.isTrue(h.mapVal("flag"))
+     * </pre>
+     */
+    public boolean isTrue(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj instanceof Boolean) {
+            return ((Boolean) obj).booleanValue();
+        }
+        if (obj instanceof String) {
+            return Boolean.parseBoolean((String) obj);
+        }
+        return true;
+    }
+
+    /**
+     * String non-emptiness for if-conditions: null is false, otherwise checks
+     * that toString() is non-empty.
+     * <pre>
+     *   // LAL:  if (parsed.name)    →  h.isNotEmpty(h.mapVal("name"))
+     * </pre>
+     */
+    public boolean isNotEmpty(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj instanceof String) {
+            return !((String) obj).isEmpty();
+        }
+        return !obj.toString().isEmpty();
+    }
+
+    /**
+     * Primitive boolean overload — needed when chained methods (e.g.
+     * {@code .endsWith()}) return primitive {@code boolean} which Javassist
+     * cannot auto-box to match {@code isNotEmpty(Object)}.
+     */
+    public boolean isNotEmpty(final boolean value) {
+        return value;
+    }
+
+    /**
+     * Primitive boolean overload for {@link #isTrue(Object)}.
+     */
+    public boolean isTrue(final boolean value) {
+        return value;
+    }
+
+    // ==================== Safe navigation ====================
+
+    /**
+     * Null-safe {@code ?.toString()}: returns null when input is null.
+     * <pre>
+     *   // LAL:  parsed?.x?.toString()    →  h.toString(h.mapVal("x"))
+     * </pre>
+     */
+    public String toString(final Object obj) {
+        return obj == null ? null : obj.toString();
+    }
+
+    /**
+     * Null-safe {@code ?.trim()}: returns null when input is null.
+     * <pre>
+     *   // LAL:  parsed?.x?.trim()    →  h.trim(h.mapVal("x"))
+     * </pre>
+     */
+    public String trim(final Object obj) {
+        return obj == null ? null : obj.toString().trim();
+    }
+
+}
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/DSL.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/DSL.java
new file mode 100644
index 0000000000..24e6936dfa
--- /dev/null
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/DSL.java
@@ -0,0 +1,90 @@
+/*
+ * 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.v2.dsl;
+
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.skywalking.apm.network.logging.v3.LogData;
+import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALClassGenerator;
+import org.apache.skywalking.oap.log.analyzer.v2.dsl.spec.filter.FilterSpec;
+import org.apache.skywalking.oap.log.analyzer.v2.provider.LogAnalyzerModuleConfig;
+import org.apache.skywalking.oap.server.library.module.ModuleManager;
+import org.apache.skywalking.oap.server.library.module.ModuleStartException;
+
+/**
+ * DSL compiles a LAL (Log Analysis Language) expression string into a
+ * {@link LalExpression} object and wraps it with runtime state management.
+ *
+ * <p>One DSL instance is created per LAL rule entry defined in a {@code .yaml}
+ * config file under {@code lal/}. Instances are compiled once at startup and
+ * reused for every incoming log. This class is immutable and thread-safe —
+ * per-log state is passed as a parameter to {@link #evaluate(ExecutionContext)}.
+ */
+@Slf4j
+@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+public class DSL {
+    private final String ruleName;
+    private final LalExpression expression;
+    private final FilterSpec filterSpec;
+
+    public static DSL of(final ModuleManager moduleManager,
+                         final LogAnalyzerModuleConfig config,
+                         final String dsl) throws ModuleStartException {
+        return of(moduleManager, config, dsl, null, "unknown", null);
+    }
+
+    public static DSL of(final ModuleManager moduleManager,
+                         final LogAnalyzerModuleConfig config,
+                         final String dsl,
+                         final Class<?> extraLogType,
+                         final String ruleName) throws ModuleStartException {
+        return of(moduleManager, config, dsl, extraLogType, ruleName, null);
+    }
+
+    public static DSL of(final ModuleManager moduleManager,
+                         final LogAnalyzerModuleConfig config,
+                         final String dsl,
+                         final Class<?> extraLogType,
+                         final String ruleName,
+                         final String yamlSource) throws ModuleStartException {
+        try {
+            final LALClassGenerator generator = new LALClassGenerator();
+            generator.setExtraLogType(extraLogType);
+            generator.setClassNameHint(ruleName);
+            generator.setYamlSource(yamlSource);
+            final LalExpression expression = generator.compile(dsl);
+            final FilterSpec filterSpec = new FilterSpec(moduleManager, config);
+            return new DSL(ruleName, expression, filterSpec);
+        } catch (Exception e) {
+            throw new ModuleStartException(
+                "Failed to compile LAL expression: " + dsl, e);
+        }
+    }
+
+    public void evaluate(final ExecutionContext ctx) {
+        if (log.isDebugEnabled()) {
+            final LogData.Builder logData = ctx.log();
+            log.debug("[LAL] rule={}, class={}, service={}, instance={}, endpoint={}, bodyType={}",
+                ruleName, expression.getClass().getName(),
+                logData.getService(), logData.getServiceInstance(),
+                logData.getEndpoint(), logData.getBody().getContentCase());
+        }
+        expression.execute(filterSpec, ctx);
+    }
+}
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/v2/dsl/ExecutionContext.java
similarity index 56%
copy from oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/Binding.java
copy to oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/ExecutionContext.java
index 763dd64d28..449a13cc80 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/v2/dsl/ExecutionContext.java
@@ -13,15 +13,12 @@
  * 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;
+package org.apache.skywalking.oap.log.analyzer.v2.dsl;
 
 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;
@@ -29,48 +26,58 @@ import java.util.concurrent.atomic.AtomicReference;
 import java.util.regex.Matcher;
 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.meter.analyzer.v2.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)}.
+ * Mutable property storage for a single LAL script execution cycle.
+ *
+ * <p>A new ExecutionContext is created for each incoming log. It carries all
+ * per-log state through the compiled LAL pipeline:
+ * <ul>
+ *   <li>{@code log} — the incoming {@code LogData.Builder}</li>
+ *   <li>{@code parsed} — structured data extracted by json/text/yaml parsers</li>
+ *   <li>{@code save}/{@code abort} — control flags set by extractor/sink logic</li>
+ *   <li>{@code metrics_container} — optional list for LAL-extracted metrics (log-MAL)</li>
+ *   <li>{@code log_container} — optional container for the built {@code Log} source object</li>
+ * </ul>
  */
-public class Binding extends groovy.lang.Binding {
+public class ExecutionContext {
     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";
 
-    public Binding() {
+    private final Map<String, Object> properties = new HashMap<>();
+
+    public ExecutionContext() {
         setProperty(KEY_PARSED, new Parsed());
     }
 
-    public Binding log(final LogData.Builder log) {
+    public void setProperty(final String name, final Object value) {
+        properties.put(name, value);
+    }
+
+    public Object getProperty(final String name) {
+        return properties.get(name);
+    }
+
+    public ExecutionContext log(final LogData.Builder log) {
         setProperty(KEY_LOG, log);
         setProperty(KEY_SAVE, true);
         setProperty(KEY_ABORT, false);
         setProperty(KEY_METRICS_CONTAINER, null);
         setProperty(KEY_LOG_CONTAINER, null);
-        parsed().log = log;
         return this;
     }
 
-    public Binding log(final LogData log) {
+    public ExecutionContext log(final LogData log) {
         return log(log.toBuilder());
     }
 
@@ -78,7 +85,7 @@ public class Binding extends groovy.lang.Binding {
         return (LogData.Builder) getProperty(KEY_LOG);
     }
 
-    public Binding extraLog(final Message extraLog) {
+    public ExecutionContext extraLog(final Message extraLog) {
         parsed().extraLog = extraLog;
         return this;
     }
@@ -87,12 +94,12 @@ public class Binding extends groovy.lang.Binding {
         return parsed().getExtraLog();
     }
 
-    public Binding parsed(final Matcher parsed) {
+    public ExecutionContext parsed(final Matcher parsed) {
         parsed().matcher = parsed;
         return this;
     }
 
-    public Binding parsed(final Map<String, Object> parsed) {
+    public ExecutionContext parsed(final Map<String, Object> parsed) {
... 53457 lines suppressed ...