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

wusheng pushed a commit to branch feature/lal-def-variables
in repository https://gitbox.apache.org/repos/asf/skywalking.git

commit f4c1462df844cbae1aa284f1b36b134eddd4a397
Author: Wu Sheng <[email protected]>
AuthorDate: Tue Mar 10 22:04:54 2026 +0800

    LAL: add def local variable support with toJson/toJsonArray and type cast
    
    Add `def` keyword to LAL grammar for declaring local variables in
    extractor and filter blocks. Type is inferred from the initializer
    expression at compile time, with optional `as` cast for type narrowing
    (supports built-in types and fully qualified class names).
    
    Split monolithic LALClassGeneratorTest into 5 scoped test classes plus
    a shared base. Add EnvoyAlsLalTest in envoy receiver module for
    ALS-specific proto chain tests with generated .class output.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 docs/en/changes/changes.md                         |   2 +
 docs/en/concepts-and-designs/lal.md                |  88 ++-
 oap-server/analyzer/log-analyzer/CLAUDE.md         |  71 ++-
 oap-server/analyzer/log-analyzer/pom.xml           |   5 +
 .../apache/skywalking/lal/rt/grammar/LALLexer.g4   |   3 +
 .../apache/skywalking/lal/rt/grammar/LALParser.g4  |  15 +-
 .../log/analyzer/v2/compiler/LALBlockCodegen.java  | 327 ++++++++++-
 .../analyzer/v2/compiler/LALClassGenerator.java    |  45 +-
 .../log/analyzer/v2/compiler/LALScriptModel.java   |  17 +
 .../log/analyzer/v2/compiler/LALScriptParser.java  |  21 +
 .../analyzer/v2/compiler/rt/LalRuntimeHelper.java  | 117 ++++
 .../v2/compiler/LALClassGeneratorBasicTest.java    | 109 ++++
 .../compiler/LALClassGeneratorConditionTest.java   | 186 ++++++
 .../v2/compiler/LALClassGeneratorDefTest.java      | 220 +++++++
 .../compiler/LALClassGeneratorExtractorTest.java   | 269 +++++++++
 .../v2/compiler/LALClassGeneratorSinkTest.java     | 113 ++++
 .../v2/compiler/LALClassGeneratorTest.java         | 641 ---------------------
 .../v2/compiler/LALClassGeneratorTestBase.java     |  68 +++
 .../v2/compiler/LALExpressionExecutionTest.java    |  44 +-
 .../analyzer/v2/compiler/LALScriptParserTest.java  | 117 ++++
 .../envoy/persistence/EnvoyAlsLalTest.java         | 368 ++++++++++++
 .../feature-cases/execution-def.input.data         |  68 +++
 .../lal/test-lal/feature-cases/execution-def.yaml  |  99 ++++
 23 files changed, 2351 insertions(+), 662 deletions(-)

diff --git a/docs/en/changes/changes.md b/docs/en/changes/changes.md
index 9c7aeb6223..9bcbd42cd2 100644
--- a/docs/en/changes/changes.md
+++ b/docs/en/changes/changes.md
@@ -26,6 +26,8 @@
   - All bundled LAL scripts (`mysql-slowsql.yaml`, `pgsql-slowsql.yaml`, 
`redis-slowsql.yaml`, `envoy-als.yaml`, `k8s-service.yaml`, `mesh-dp.yaml`) 
have been updated.
   - Users with custom LAL scripts using `slowSql {}` or `sampledTrace {}` must 
migrate to the new syntax. See [LAL 
documentation](../concepts-and-designs/lal.md#output-type).
   - Rename `ExtractorSpec` to `MetricExtractor` — now only handles LAL 
`metrics {}` blocks. Standard field setters (service, layer, timestamp, etc.) 
are compiled as direct setter calls on the output builder.
+  - Add `def` local variable support in LAL extractor (and filter level). 
Supports `toJson()` and `toJsonArray()` built-in functions for converting 
strings, Maps, and protobuf `Struct` to Gson JSON objects. Variables support 
null-safe navigation (`?.`), method chaining with compile-time type inference, 
and explicit type cast via `as` (built-in types or fully qualified class names, 
e.g., `def resp = parsed?.response as 
io.envoyproxy.envoy.data.accesslog.v3.HTTPResponseProperties`).
+  - **Breaking Change** — `LALOutputBuilder.init()` signature changed from 
`init(LogData, NamingControl)` to `init(LogData, Optional<Object> extraLog, 
NamingControl)`. The `extraLog` parameter carries the typed input object (e.g., 
`HTTPAccessLogEntry` for envoy access logs) so that output builders can access 
protocol-specific fields. Custom `LALOutputBuilder` implementations must update 
their `init()` method signature.
 * 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.
diff --git a/docs/en/concepts-and-designs/lal.md 
b/docs/en/concepts-and-designs/lal.md
index 626b66f17d..6f5b07e01c 100644
--- a/docs/en/concepts-and-designs/lal.md
+++ b/docs/en/concepts-and-designs/lal.md
@@ -145,6 +145,89 @@ filter {
 Extractors aim to extract metadata from the logs. The metadata can be a 
service name, a service instance name, an
 endpoint name, or even a trace ID, all of which can be associated with the 
existing traces and metrics.
 
+#### Local variables (`def`)
+
+You can use `def` to declare local variables in the extractor (or at the 
filter level). This is useful when an
+expression is reused multiple times, or when you want to break a long chain 
into readable steps.
+
+The syntax is:
+
+```
+def variableName = expression
+def variableName = expression as TypeName
+```
+
+The variable type is inferred from the initializer expression at compile time. 
`def` is not limited to JSON — it works
+with any value access expression whose type is resolvable on the classpath, 
including protobuf getter chains,
+`log.*` fields, and Gson JSON method chains. Subsequent method calls on the 
variable are validated at compile time
+against the inferred type.
+
+You can optionally add an explicit `as` type cast to narrow the variable type. 
The cast type can be a built-in
+type (`String`, `Long`, `Integer`, `Boolean`) or a fully qualified class name:
+
+```
+def value = someExpression as com.example.MyType
+```
+
+This is useful when the compiler infers a type that is too general (e.g., 
`Object` from a generic API return)
+and you know the concrete runtime type. The cast tells the compiler which type 
to use for subsequent method chain
+validation. Note that `as` performs a Java cast — it does **not** convert 
between types. For JSON conversion, use
+`toJson()` or `toJsonArray()` instead.
+
+The FQCN must be resolvable on the classpath at compile time. If the class is 
not found, the OAP server will fail
+to start.
+
+Two built-in conversion functions are provided for JSON interoperability:
+
+- `toJson(expr)` — converts a value to a Gson `JsonObject`. Works with JSON 
strings, `Map`, and protobuf `Struct`.
+- `toJsonArray(expr)` — converts a value to a Gson `JsonArray`. Works with 
JSON array strings.
+
+After declaration, the variable can be used in subsequent expressions with 
full null-safe navigation support (`?.`).
+
+Example — extracting fields from a protobuf input type (no JSON conversion 
needed):
+
+```
+filter {
+    extractor {
+        def resp = parsed?.response
+        tag 'status.code': resp?.responseCode?.value
+        tag 'resp.flags': resp?.responseCodeDetails
+    }
+    sink {}
+}
+```
+
+Example — extracting JWT claims from envoy access log filter metadata via 
`toJson()`:
+
+```
+filter {
+    extractor {
+        def jwt = toJson(parsed?.commonProperties?.metadata
+            ?.filterMetadataMap?.get("envoy.filters.http.jwt_authn"))
+        def payload = jwt?.getAsJsonObject("payload")
+        if (payload != null) {
+            tag 'email': payload?.get("email")?.getAsString()
+            tag 'group': payload?.get("group")?.getAsString()
+        }
+    }
+    sink {}
+}
+```
+
+Example — parsing a JSON log body field into a structured object:
+
+```
+filter {
+    json {}
+    extractor {
+        def config = toJson(parsed.metadata)
+        tag 'env': config?.get("env")?.getAsString()
+        tag 'region': 
config?.getAsJsonObject("location")?.get("region")?.getAsString()
+    }
+    sink {}
+}
+```
+
 #### Standard fields
 
 - `service`
@@ -473,7 +556,7 @@ LAL supports two kinds of output types:
 | Output path | Base type | How it works |
 |---|---|---|
 | **Log path** | Subclass of `AbstractLog` | The sink populates standard log 
fields (service, instance, endpoint, tags, body, etc.) from `LogData` and 
persists via `SourceReceiver` |
-| **Builder path** | Implements `LALOutputBuilder` | The sink creates the 
builder, calls `init(LogData, NamingControl)` to pre-populate standard fields, 
applies output field values via setters, then calls `complete(SourceReceiver)` 
to validate and dispatch |
+| **Builder path** | Implements `LALOutputBuilder` | The sink creates the 
builder, calls `init(LogData, Optional<Object> extraLog, NamingControl)` to 
pre-populate standard fields, applies output field values via setters, then 
calls `complete(SourceReceiver)` to validate and dispatch |
 
 The builder path is used when the output type implements the 
`LALOutputBuilder` interface. This is how SkyWalking's
 built-in slow SQL and sampled trace features work.
@@ -537,8 +620,9 @@ public class MyCustomBuilder implements LALOutputBuilder {
     public String name() { return NAME; }
 
     @Override
-    public void init(LogData logData, NamingControl namingControl) {
+    public void init(LogData logData, Optional<Object> extraLog, NamingControl 
namingControl) {
         // Pre-populate from LogData (service, timestamp, traceId, etc.)
+        // extraLog contains the typed input object (e.g., a protobuf message) 
if available
     }
 
     @Override
diff --git a/oap-server/analyzer/log-analyzer/CLAUDE.md 
b/oap-server/analyzer/log-analyzer/CLAUDE.md
index 6e75231e03..eb3c134c65 100644
--- a/oap-server/analyzer/log-analyzer/CLAUDE.md
+++ b/oap-server/analyzer/log-analyzer/CLAUDE.md
@@ -50,8 +50,13 @@ oap-server/analyzer/log-analyzer/
 
   src/test/java/.../compiler/
     LALScriptParserTest.java            — 19 parser tests
-    LALClassGeneratorTest.java          — 35 generator tests
-    LALExpressionExecutionTest.java     — 25 data-driven execution tests (from 
YAML + .data.yaml)
+    LALClassGeneratorTestBase.java      — shared base: fresh generator per 
test, .class output, naming
+    LALClassGeneratorBasicTest.java     — 10 tests: minimal compile, parsers, 
source gen, errors
+    LALClassGeneratorConditionTest.java — 10 tests: tag(), safe-nav, 
if-blocks, else-if
+    LALClassGeneratorExtractorTest.java — 10 tests: ProcessRegistry, metrics, 
inputType, outputType
+    LALClassGeneratorDefTest.java       — 7 tests: def variables, 
toJson/toJsonArray
+    LALClassGeneratorSinkTest.java      — 5 tests: sampler, rateLimit, 
interpolated IDs
+    LALExpressionExecutionTest.java     — 25+ data-driven execution tests 
(from YAML + .data.yaml)
 ```
 
 ## Package & Class Naming
@@ -105,6 +110,68 @@ All generated methods include a `LocalVariableTable` 
attribute for debugger/deco
 
 LVT entries are added via `PrivateMethod` inner class which carries both 
source code and variable descriptors.
 
+## Local Variables (`def`)
+
+The `def` keyword declares local variables in the extractor (or filter level). 
The grammar rule:
+
+```
+defStatement: DEF IDENTIFIER ASSIGN valueAccess typeCast? ;
+```
+
+The optional `typeCast` supports built-in types (`String`, `Long`, `Integer`, 
`Boolean`) and
+fully qualified class names (`as com.example.MyType`). The FQCN is resolved 
via `Class.forName()`
+at compile time. If not found, compilation fails with 
`IllegalArgumentException`.
+
+### Type inference
+
+The variable type is inferred from the initializer expression:
+
+| Initializer | Inferred type | Generated code |
+|---|---|---|
+| `toJson(expr)` | `JsonObject` | `h.toJsonObject(expr)` |
+| `toJsonArray(expr)` | `JsonArray` | `h.toJsonArray(expr)` |
+| General value access | Last resolved type via reflection | Standard value 
access codegen |
+
+Built-in functions are registered in `BUILTIN_FUNCTIONS` map in 
`LALBlockCodegen`.
+
+### Chained def variables
+
+A `def` variable can be initialized from another `def` variable's method chain:
+
+```
+def jwt = 
toJson(parsed?.commonProperties?.metadata?.filterMetadataMap?.get("envoy.filters.http.jwt_authn"))
+def payload = jwt?.getAsJsonObject("payload")
+tag 'email': payload?.get("email")?.getAsString()
+```
+
+The general value access path in `generateDefStatement()` recognizes `jwt` as 
a def variable,
+delegates to `generateDefVarChain()` which uses reflection to resolve the 
chain, and
+`genCtx.lastResolvedType` captures the resolved type (`JsonObject` in this 
case).
+
+### Runtime helpers
+
+`LalRuntimeHelper` provides `toJsonObject()` and `toJsonArray()` overloads:
+
+| Method | Input type | Conversion |
+|---|---|---|
+| `toJsonObject(String)` | JSON string | 
`JsonParser.parseString().getAsJsonObject()` |
+| `toJsonObject(Map)` | Map (from JSON/YAML parser) | Recursive Gson 
conversion |
+| `toJsonObject(Struct)` | Protobuf `Struct` | Recursive field conversion 
preserving nested structures |
+| `toJsonObject(Object)` | Any (fallback) | Delegates to above based on 
runtime type |
+| `toJsonArray(String)` | JSON array string | 
`JsonParser.parseString().getAsJsonArray()` |
+| `toJsonArray(Object)` | Any (fallback) | String fallback |
+
+All methods return `null` for `null` input (null-safe).
+
+### Code generation
+
+Def variables are stored in `genCtx.localVars` map (name → `LocalVarInfo` with 
Java variable name
+and resolved type). Variable declarations are emitted at method top via 
`genCtx.localVarDecls`;
+assignments are emitted at the point where `def` appears in the DSL.
+
+Java variable names follow the pattern `_d0`, `_d1`, etc. LVT entries are 
added for debugger
+visibility.
+
 ## Compile-Time Data Source Analysis
 
 The generator detects the parser type from the AST at compile time and 
generates typed value access:
diff --git a/oap-server/analyzer/log-analyzer/pom.xml 
b/oap-server/analyzer/log-analyzer/pom.xml
index 180a8435df..b0733f1592 100644
--- a/oap-server/analyzer/log-analyzer/pom.xml
+++ b/oap-server/analyzer/log-analyzer/pom.xml
@@ -47,6 +47,11 @@
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-databind</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+            <version>${gson.version}</version>
+        </dependency>
         <dependency>
             <groupId>org.antlr</groupId>
             <artifactId>antlr4-runtime</artifactId>
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
index c6edd0ad60..adca49a52f 100644
--- 
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
@@ -54,6 +54,9 @@ RPM:           'rpm';
 ENFORCER:      'enforcer';
 DROPPER:       'dropper';
 
+// Keywords - local variables
+DEF:           'def';
+
 // Keywords - control flow
 IF:            'if';
 ELSE:          'else';
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
index fa2f615e16..cf8956da0b 100644
--- 
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
@@ -54,6 +54,7 @@ filterStatement
     | sinkBlock
     | ifStatement
     | abortBlock
+    | defStatement
     ;
 
 // ==================== Parser blocks ====================
@@ -116,7 +117,8 @@ extractorContent
     ;
 
 extractorStatement
-    : serviceStatement
+    : defStatement
+    | serviceStatement
     | instanceStatement
     | endpointStatement
     | layerStatement
@@ -130,6 +132,10 @@ extractorStatement
     | outputFieldStatement
     ;
 
+defStatement
+    : DEF IDENTIFIER ASSIGN valueAccess typeCast?
+    ;
+
 serviceStatement
     : SERVICE valueAccess typeCast?
     ;
@@ -368,7 +374,11 @@ functionArg
 // ==================== Type cast ====================
 
 typeCast
-    : AS (STRING_TYPE | LONG_TYPE | INTEGER_TYPE | BOOLEAN_TYPE)
+    : AS (STRING_TYPE | LONG_TYPE | INTEGER_TYPE | BOOLEAN_TYPE | 
qualifiedName)
+    ;
+
+qualifiedName
+    : IDENTIFIER (DOT IDENTIFIER)*
     ;
 
 // ==================== Common ====================
@@ -384,6 +394,7 @@ anyIdentifier
     | NAME | VALUE | LABELS
     | SAMPLER | RATE_LIMIT | RPM | ENFORCER | DROPPER
     | TEXT | JSON | YAML | FILTER | EXTRACTOR | SINK | ABORT
+    | DEF
     ;
 
 boolValue
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
index 3d10117f00..7e4e4df7f6 100644
--- 
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
@@ -17,7 +17,10 @@
 
 package org.apache.skywalking.oap.log.analyzer.v2.compiler;
 
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import org.apache.skywalking.apm.network.logging.v3.LogData;
@@ -41,6 +44,15 @@ final class LALBlockCodegen {
     private static final String PROCESS_REGISTRY =
         
"org.apache.skywalking.oap.meter.analyzer.v2.dsl.registry.ProcessRegistry";
 
+    // Built-in function registry for def variable type inference.
+    // Maps DSL function name → [runtime helper method, return type].
+    static final Map<String, Object[]> BUILTIN_FUNCTIONS = new HashMap<>();
+
+    static {
+        BUILTIN_FUNCTIONS.put("toJson", new Object[]{"h.toJsonObject", 
JsonObject.class});
+        BUILTIN_FUNCTIONS.put("toJsonArray", new Object[]{"h.toJsonArray", 
JsonArray.class});
+    }
+
     private LALBlockCodegen() {
         // utility class
     }
@@ -92,6 +104,12 @@ final class LALBlockCodegen {
                 "L" + outTypeName.replace('.', '/') + ";"});
         }
 
+        // Add local var declarations from def statements
+        if (genCtx.localVarDecls.length() > 0) {
+            body.append(genCtx.localVarDecls);
+            lvtVars.addAll(genCtx.localVarLvtVars);
+        }
+
         // Add LVT entry for _metrics if any metrics block exists
         if (hasMetricsBlock(block.getStatements())) {
             lvtVars.add(new String[]{"_metrics",
@@ -128,6 +146,9 @@ final class LALBlockCodegen {
             } else if (stmt instanceof LALScriptModel.OutputFieldAssignment) {
                 generateOutputFieldAssignment(
                     sb, (LALScriptModel.OutputFieldAssignment) stmt, genCtx);
+            } else if (stmt instanceof LALScriptModel.DefStatement) {
+                generateDefStatement(
+                    sb, (LALScriptModel.DefStatement) stmt, genCtx);
             }
         }
     }
@@ -861,6 +882,17 @@ final class LALBlockCodegen {
             return;
         }
 
+        // Check for def variable reference
+        if (!value.getSegments().isEmpty()) {
+            final String primaryName = value.getSegments().get(0);
+            final LALClassGenerator.LocalVarInfo localVar =
+                genCtx.localVars.get(primaryName);
+            if (localVar != null) {
+                generateDefVarChain(sb, localVar, chain, genCtx);
+                return;
+            }
+        }
+
         // Fallback for unknown primary
         if (chain.isEmpty()) {
             sb.append("null");
@@ -1140,6 +1172,283 @@ final class LALBlockCodegen {
         return prevVar;
     }
 
+    // ==================== Def statement codegen ====================
+
+    static void generateDefStatement(final StringBuilder sb,
+                                      final LALScriptModel.DefStatement def,
+                                      final LALClassGenerator.GenCtx genCtx) {
+        final LALScriptModel.ValueAccess init = def.getInitializer();
+        final String varName = def.getVarName();
+        final String javaVar = "_d" + genCtx.localVarCounter++;
+
+        // Determine type and generate initializer expression
+        Class<?> resolvedType;
+        final StringBuilder initExpr = new StringBuilder();
+
+        if (init.getFunctionCallName() != null
+                && BUILTIN_FUNCTIONS.containsKey(init.getFunctionCallName())) {
+            // Built-in function: toJson(...), toJsonArray(...)
+            final Object[] info = 
BUILTIN_FUNCTIONS.get(init.getFunctionCallName());
+            final String helperMethod = (String) info[0];
+            resolvedType = (Class<?>) info[1];
+
+            initExpr.append(helperMethod).append("(");
+            if (!init.getFunctionCallArgs().isEmpty()) {
+                generateValueAccess(initExpr,
+                    init.getFunctionCallArgs().get(0).getValue(), genCtx);
+            } else {
+                initExpr.append("null");
+            }
+            initExpr.append(")");
+        } else {
+            // General value access — type inferred from lastResolvedType
+            generateValueAccess(initExpr, init, genCtx);
+            resolvedType = genCtx.lastResolvedType != null
+                ? genCtx.lastResolvedType : Object.class;
+            // Box primitive types for local variable declarations
+            if (resolvedType.isPrimitive()) {
+                final String boxName = 
LALCodegenHelper.boxTypeName(resolvedType);
+                if (boxName != null) {
+                    try {
+                        resolvedType = Class.forName("java.lang." + boxName);
+                    } catch (ClassNotFoundException ignored) {
+                        // keep primitive
+                    }
+                }
+            }
+        }
+
+        // Apply explicit type cast if specified (e.g., "as 
com.example.MyType")
+        final String castType = def.getCastType();
+        if (castType != null && !castType.isEmpty()) {
+            // Resolve the cast type — primitive wrapper names are handled,
+            // anything else is treated as a FQCN
+            final Class<?> castClass = resolveDefCastType(castType);
+            if (castClass != null) {
+                resolvedType = castClass;
+            }
+        }
+
+        // Register in local vars for later reference
+        genCtx.localVars.put(varName,
+            new LALClassGenerator.LocalVarInfo(javaVar, resolvedType));
+
+        // Emit declaration (placed at method top via localVarDecls)
+        genCtx.localVarDecls.append("  ").append(resolvedType.getName())
+            .append(" ").append(javaVar).append(";\n");
+        genCtx.localVarLvtVars.add(new String[]{
+            javaVar, "L" + resolvedType.getName().replace('.', '/') + ";"
+        });
+
+        // Emit assignment in body (at the point where def appears)
+        sb.append("  ").append(javaVar).append(" = ");
+        if (castType != null && !castType.isEmpty()) {
+            sb.append("(").append(resolvedType.getName()).append(") ");
+        }
+        sb.append(initExpr).append(";\n");
+    }
+
+    /**
+     * Resolves a cast type string to a {@link Class}.
+     * Handles the four built-in type names ({@code String}, {@code Long},
+     * {@code Integer}, {@code Boolean}) and fully qualified class names.
+     */
+    private static Class<?> resolveDefCastType(final String castType) {
+        switch (castType) {
+            case "String":
+                return String.class;
+            case "Long":
+                return Long.class;
+            case "Integer":
+                return Integer.class;
+            case "Boolean":
+                return Boolean.class;
+            default:
+                try {
+                    return Class.forName(castType);
+                } catch (ClassNotFoundException e) {
+                    throw new IllegalArgumentException(
+                        "def cast type not found on classpath: " + castType, 
e);
+                }
+        }
+    }
+
+    // ==================== Def variable chain codegen ====================
+
+    /**
+     * Generates typed method-chain access on a def variable.
+     * Uses reflection to resolve each method/field call and track types.
+     *
+     * @param sb output buffer
+     * @param localVar the def variable info (java var name + resolved type)
+     * @param chain the chain segments after the variable name
+     * @param genCtx codegen context
+     */
+    static void generateDefVarChain(
+            final StringBuilder sb,
+            final LALClassGenerator.LocalVarInfo localVar,
+            final List<LALScriptModel.ValueAccessSegment> chain,
+            final LALClassGenerator.GenCtx genCtx) {
+        if (chain.isEmpty()) {
+            sb.append(localVar.javaVarName);
+            genCtx.lastResolvedType = localVar.resolvedType;
+            return;
+        }
+
+        String prevExpr = localVar.javaVarName;
+        Class<?> currentType = localVar.resolvedType;
+        boolean canBeNull = true;
+
+        for (int i = 0; i < chain.size(); i++) {
+            final LALScriptModel.ValueAccessSegment seg = chain.get(i);
+            final boolean isLast = i == chain.size() - 1;
+
+            if (seg instanceof LALScriptModel.MethodSegment) {
+                final LALScriptModel.MethodSegment ms =
+                    (LALScriptModel.MethodSegment) seg;
+                final String methodName = ms.getName();
+
+                // Resolve method on currentType via reflection
+                final java.lang.reflect.Method method =
+                    resolveMethod(currentType, methodName, ms.getArguments());
+                if (method == null) {
+                    throw new IllegalArgumentException(
+                        "Cannot resolve method " + currentType.getSimpleName()
+                            + "." + methodName + "() in def variable chain");
+                }
+                final Class<?> returnType = method.getReturnType();
+                final String args = generateMethodArgs(ms.getArguments());
+
+                if (ms.isSafeNav() && canBeNull) {
+                    if (isLast && returnType.isPrimitive()) {
+                        // Primitive return with null guard
+                        final String boxName =
+                            LALCodegenHelper.boxTypeName(returnType);
+                        prevExpr = "(" + prevExpr + " == null ? null : "
+                            + boxName + ".valueOf(" + prevExpr + "."
+                            + methodName + "(" + args + ")))";
+                        currentType = returnType;
+                    } else {
+                        prevExpr = "(" + prevExpr + " == null ? null : "
+                            + prevExpr + "." + methodName + "(" + args + "))";
+                        currentType = returnType;
+                        canBeNull = true;
+                    }
+                } else {
+                    prevExpr = prevExpr + "." + methodName + "(" + args + ")";
+                    currentType = returnType;
+                    canBeNull = !returnType.isPrimitive();
+                }
+            } else if (seg instanceof LALScriptModel.FieldSegment) {
+                final LALScriptModel.FieldSegment fs =
+                    (LALScriptModel.FieldSegment) seg;
+                final String fieldName = fs.getName();
+                // Try getter first
+                final String getterName = "get"
+                    + Character.toUpperCase(fieldName.charAt(0))
+                    + fieldName.substring(1);
+                java.lang.reflect.Method getter = null;
+                try {
+                    getter = currentType.getMethod(getterName);
+                } catch (NoSuchMethodException e) {
+                    // Try direct field access name
+                    try {
+                        getter = currentType.getMethod(fieldName);
+                    } catch (NoSuchMethodException e2) {
+                        throw new IllegalArgumentException(
+                            "Cannot resolve field/getter "
+                                + currentType.getSimpleName()
+                                + "." + fieldName + " in def variable chain");
+                    }
+                }
+                final Class<?> returnType = getter.getReturnType();
+
+                if (fs.isSafeNav() && canBeNull) {
+                    if (isLast && returnType.isPrimitive()) {
+                        final String boxName =
+                            LALCodegenHelper.boxTypeName(returnType);
+                        prevExpr = "(" + prevExpr + " == null ? null : "
+                            + boxName + ".valueOf(" + prevExpr + "."
+                            + getter.getName() + "()))";
+                        currentType = returnType;
+                    } else {
+                        prevExpr = "(" + prevExpr + " == null ? null : "
+                            + prevExpr + "." + getter.getName() + "())";
+                        currentType = returnType;
+                        canBeNull = true;
+                    }
+                } else {
+                    prevExpr = prevExpr + "." + getter.getName() + "()";
+                    currentType = returnType;
+                    canBeNull = !returnType.isPrimitive();
+                }
+            } else if (seg instanceof LALScriptModel.IndexSegment) {
+                final int index = ((LALScriptModel.IndexSegment) 
seg).getIndex();
+                // Try get(int) method (e.g., JsonArray.get(int))
+                java.lang.reflect.Method getMethod = null;
+                try {
+                    getMethod = currentType.getMethod("get", int.class);
+                } catch (NoSuchMethodException e) {
+                    throw new IllegalArgumentException(
+                        "Cannot resolve index access on "
+                            + currentType.getSimpleName()
+                            + " in def variable chain");
+                }
+                final Class<?> returnType = getMethod.getReturnType();
+                if (canBeNull) {
+                    prevExpr = "(" + prevExpr + " == null ? null : "
+                        + prevExpr + ".get(" + index + "))";
+                } else {
+                    prevExpr = prevExpr + ".get(" + index + ")";
+                }
+                currentType = returnType;
+                canBeNull = true;
+            }
+        }
+
+        genCtx.lastResolvedType = currentType;
+        sb.append(prevExpr);
+    }
+
+    /**
+     * Resolves a method on the given type by name, matching argument count.
+     * For methods with String arguments (like JsonObject.get(String)),
+     * prioritizes exact match by parameter types.
+     */
+    private static java.lang.reflect.Method resolveMethod(
+            final Class<?> type, final String name,
+            final List<LALScriptModel.FunctionArg> args) {
+        final int argCount = args != null ? args.size() : 0;
+        // Try exact match with common parameter types
+        if (argCount == 1) {
+            try {
+                return type.getMethod(name, String.class);
+            } catch (NoSuchMethodException ignored) {
+                // fall through
+            }
+            try {
+                return type.getMethod(name, int.class);
+            } catch (NoSuchMethodException ignored) {
+                // fall through
+            }
+        }
+        if (argCount == 0) {
+            try {
+                return type.getMethod(name);
+            } catch (NoSuchMethodException ignored) {
+                // fall through
+            }
+        }
+        // Fallback: find by name and arg count
+        for (final java.lang.reflect.Method m : type.getMethods()) {
+            if (m.getName().equals(name)
+                    && m.getParameterCount() == argCount) {
+                return m;
+            }
+        }
+        return null;
+    }
+
     // ==================== ProcessRegistry ====================
 
     static void generateProcessRegistryCall(
@@ -1175,23 +1484,21 @@ final class LALBlockCodegen {
 
     static String appendMethodSegment(final String current,
                                        final LALScriptModel.MethodSegment ms) {
+        final String mn = ms.getName();
+        final String args = ms.getArguments().isEmpty()
+            ? "" : generateMethodArgs(ms.getArguments());
         if (ms.isSafeNav()) {
-            final String mn = ms.getName();
+            // Special-cased helpers for common safe-nav methods on Object
             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 + "()");
             }
+            // General safe-nav: null guard with ternary
+            return "(" + current + " == null ? null : "
+                + current + "." + mn + "(" + args + "))";
         } else {
-            if (ms.getArguments().isEmpty()) {
-                return current + "." + ms.getName() + "()";
-            } else {
-                return current + "." + ms.getName() + "("
-                    + generateMethodArgs(ms.getArguments()) + ")";
-            }
+            return current + "." + mn + "(" + args + ")";
         }
     }
 
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
index 78f20f844a..62aa58de5a 100644
--- 
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
@@ -85,6 +85,18 @@ public final class LALClassGenerator {
         }
     }
 
+    static class LocalVarInfo {
+        final String javaVarName;
+        final Class<?> resolvedType;
+        final String descriptor;
+
+        LocalVarInfo(final String javaVarName, final Class<?> resolvedType) {
+            this.javaVarName = javaVarName;
+            this.resolvedType = resolvedType;
+            this.descriptor = "L" + resolvedType.getName().replace('.', '/') + 
";";
+        }
+    }
+
     static class GenCtx {
         final ParserType parserType;
         final Class<?> inputType;
@@ -107,6 +119,13 @@ public final class LALClassGenerator {
         int protoVarCounter;
         boolean usedProtoAccess;
 
+        // Local variables from def statements.
+        // Maps user-chosen name (e.g., "metadata") to type info.
+        final Map<String, LocalVarInfo> localVars = new HashMap<>();
+        int localVarCounter;
+        final StringBuilder localVarDecls = new StringBuilder();
+        final List<String[]> localVarLvtVars = new ArrayList<>();
+
         GenCtx(final ParserType parserType, final Class<?> inputType,
                final Class<?> outputType) {
             this.parserType = parserType;
@@ -131,6 +150,10 @@ public final class LALClassGenerator {
             protoVarDecls.setLength(0);
             protoVarCounter = 0;
             usedProtoAccess = false;
+            localVars.clear();
+            localVarCounter = 0;
+            localVarDecls.setLength(0);
+            localVarLvtVars.clear();
         }
 
         Object[] saveProtoVarState() {
@@ -139,7 +162,11 @@ public final class LALClassGenerator {
                 new ArrayList<>(protoLvtVars),
                 protoVarDecls.toString(),
                 protoVarCounter,
-                usedProtoAccess
+                usedProtoAccess,
+                new HashMap<>(localVars),
+                localVarCounter,
+                localVarDecls.toString(),
+                new ArrayList<>(localVarLvtVars)
             };
         }
 
@@ -153,6 +180,13 @@ public final class LALClassGenerator {
             protoVarDecls.append((String) state[2]);
             protoVarCounter = (Integer) state[3];
             usedProtoAccess = (Boolean) state[4];
+            localVars.clear();
+            localVars.putAll((Map<String, LocalVarInfo>) state[5]);
+            localVarCounter = (Integer) state[6];
+            localVarDecls.setLength(0);
+            localVarDecls.append((String) state[7]);
+            localVarLvtVars.clear();
+            localVarLvtVars.addAll((List<String[]>) state[8]);
         }
     }
 
@@ -455,6 +489,7 @@ public final class LALClassGenerator {
             }
             execLvt.addAll(genCtx.protoLvtVars);
         }
+        execLvt.addAll(genCtx.localVarLvtVars);
         addLocalVariableTable(execMethod, className,
             execLvt.toArray(new String[0][]));
         addLineNumberTable(execMethod, 3); // slot 0=this, 1=filterSpec, 2=ctx
@@ -517,6 +552,11 @@ public final class LALClassGenerator {
             sb.append(genCtx.protoVarDecls);
         }
 
+        // Insert local var declarations from def statements at execute level
+        if (genCtx.localVarDecls.length() > 0) {
+            sb.append(genCtx.localVarDecls);
+        }
+
         sb.append(bodyContent);
         sb.append("}\n");
         return sb.toString();
@@ -552,6 +592,9 @@ public final class LALClassGenerator {
             }
         } else if (stmt instanceof LALScriptModel.IfBlock) {
             generateTopLevelIfBlock(sb, (LALScriptModel.IfBlock) stmt, genCtx);
+        } else if (stmt instanceof LALScriptModel.DefStatement) {
+            LALBlockCodegen.generateDefStatement(
+                sb, (LALScriptModel.DefStatement) stmt, genCtx);
         }
     }
 
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
index e20232e56b..9f590225ae 100644
--- 
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
@@ -82,6 +82,23 @@ public final class LALScriptModel {
     public static final class AbortStatement implements FilterStatement {
     }
 
+    // ==================== Local variable declaration ====================
+
+    @Getter
+    public static final class DefStatement implements FilterStatement, 
ExtractorStatement {
+        private final String varName;
+        private final ValueAccess initializer;
+        private final String castType;
+
+        public DefStatement(final String varName,
+                            final ValueAccess initializer,
+                            final String castType) {
+            this.varName = varName;
+            this.initializer = initializer;
+            this.castType = castType;
+        }
+    }
+
     // ==================== Extractor block ====================
 
     @Getter
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
index fd91feb3c5..6a526e282e 100644
--- 
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
@@ -29,6 +29,7 @@ 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.DefStatement;
 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;
@@ -134,6 +135,9 @@ public final class LALScriptParser {
         if (ctx.abortBlock() != null) {
             return new AbortStatement();
         }
+        if (ctx.defStatement() != null) {
+            return visitDefStatement(ctx.defStatement());
+        }
         // ifStatement
         return visitIfStatement(ctx.ifStatement());
     }
@@ -190,6 +194,9 @@ public final class LALScriptParser {
 
     private static ExtractorStatement visitExtractorStatement(
             final LALParser.ExtractorStatementContext ctx) {
+        if (ctx.defStatement() != null) {
+            return (ExtractorStatement) visitDefStatement(ctx.defStatement());
+        }
         if (ctx.serviceStatement() != null) {
             return visitFieldAssignment(FieldType.SERVICE, 
ctx.serviceStatement().valueAccess(),
                 ctx.serviceStatement().typeCast());
@@ -250,6 +257,17 @@ public final class LALScriptParser {
         return new LALScriptModel.OutputFieldAssignment(fieldName, value, 
castType);
     }
 
+    // ==================== Def statement ====================
+
+    private static DefStatement visitDefStatement(
+            final LALParser.DefStatementContext ctx) {
+        final String varName = ctx.IDENTIFIER().getText();
+        final ValueAccess initializer = visitValueAccess(ctx.valueAccess());
+        final String castType = ctx.typeCast() != null
+            ? extractCastType(ctx.typeCast()) : null;
+        return new DefStatement(varName, initializer, castType);
+    }
+
     // ==================== Tag statement ====================
 
     private static TagAssignment visitTagStatement(final 
LALParser.TagStatementContext ctx) {
@@ -741,6 +759,9 @@ public final class LALScriptParser {
         if (ctx.BOOLEAN_TYPE() != null) {
             return "Boolean";
         }
+        if (ctx.qualifiedName() != null) {
+            return ctx.qualifiedName().getText();
+        }
         return null;
     }
 
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
index 0c2a11b3ad..83c6494699 100644
--- 
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
@@ -17,6 +17,14 @@
 
 package org.apache.skywalking.oap.log.analyzer.v2.compiler.rt;
 
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonPrimitive;
+import com.google.protobuf.ListValue;
+import com.google.protobuf.Struct;
+import com.google.protobuf.Value;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.List;
@@ -180,6 +188,115 @@ public final class LalRuntimeHelper {
         return "";
     }
 
+    // ==================== JSON conversion (for def variables) 
====================
+    //
+    // toJsonObject: converts to JsonObject. Overloaded for Struct, String, 
Object.
+    // toJsonArray:  converts to JsonArray.  Overloaded for String, Object.
+    //
+    // Primary use case: extract JWT claims from envoy ALS filter_metadata:
+    //   def jwt = toJson(parsed?.commonProperties?.metadata
+    //       ?.filterMetadataMap?.get("envoy.filters.http.jwt_authn"))
+    //   tag 'email': 
jwt?.getAsJsonObject("payload")?.get("email")?.getAsString()
+
+    /**
+     * Converts a protobuf {@link Struct} to a Gson {@link JsonObject}.
+     * Recursively converts nested Struct/Value/ListValue to Gson types.
+     */
+    public JsonObject toJsonObject(final Struct struct) {
+        if (struct == null) {
+            return null;
+        }
+        return structToJsonObject(struct);
+    }
+
+    /**
+     * Parses a JSON string into a {@link JsonObject}.
+     */
+    public JsonObject toJsonObject(final String s) {
+        if (s == null || s.isEmpty()) {
+            return null;
+        }
+        final JsonElement el = JsonParser.parseString(s);
+        return el.isJsonObject() ? el.getAsJsonObject() : null;
+    }
+
+    /**
+     * Fallback for {@code Map.get()} erasure — dispatches to typed overloads.
+     */
+    public JsonObject toJsonObject(final Object obj) {
+        if (obj == null) {
+            return null;
+        }
+        if (obj instanceof Struct) {
+            return toJsonObject((Struct) obj);
+        }
+        if (obj instanceof String) {
+            return toJsonObject((String) obj);
+        }
+        return null;
+    }
+
+    /**
+     * Parses a JSON string into a {@link JsonArray}.
+     */
+    public JsonArray toJsonArray(final String s) {
+        if (s == null || s.isEmpty()) {
+            return null;
+        }
+        final JsonElement el = JsonParser.parseString(s);
+        return el.isJsonArray() ? el.getAsJsonArray() : null;
+    }
+
+    /**
+     * Fallback for {@code Map.get()} erasure.
+     */
+    public JsonArray toJsonArray(final Object obj) {
+        if (obj == null) {
+            return null;
+        }
+        if (obj instanceof String) {
+            return toJsonArray((String) obj);
+        }
+        return null;
+    }
+
+    // ==================== Struct → Gson recursive conversion 
====================
+
+    private static JsonObject structToJsonObject(final Struct struct) {
+        final JsonObject obj = new JsonObject();
+        for (final Map.Entry<String, Value> entry : 
struct.getFieldsMap().entrySet()) {
+            obj.add(entry.getKey(), valueToJsonElement(entry.getValue()));
+        }
+        return obj;
+    }
+
+    private static JsonElement valueToJsonElement(final Value value) {
+        switch (value.getKindCase()) {
+            case STRING_VALUE:
+                return new JsonPrimitive(value.getStringValue());
+            case NUMBER_VALUE:
+                return new JsonPrimitive(value.getNumberValue());
+            case BOOL_VALUE:
+                return new JsonPrimitive(value.getBoolValue());
+            case STRUCT_VALUE:
+                return structToJsonObject(value.getStructValue());
+            case LIST_VALUE:
+                return listValueToJsonArray(value.getListValue());
+            case NULL_VALUE:
+            case KIND_NOT_SET:
+            default:
+                return com.google.gson.JsonNull.INSTANCE;
+        }
+    }
+
+    private static JsonArray listValueToJsonArray(final ListValue listValue) {
+        final JsonArray arr = new JsonArray();
+        for (final Value v : listValue.getValuesList()) {
+            arr.add(valueToJsonElement(v));
+        }
+        return arr;
+    }
+
     // ==================== Timestamp parsing ====================
 
     public long parseTimestamp(final String timestamp, final String 
formatPattern) {
diff --git 
a/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGeneratorBasicTest.java
 
b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGeneratorBasicTest.java
new file mode 100644
index 0000000000..29f4103319
--- /dev/null
+++ 
b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGeneratorBasicTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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 org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Basic compilation and error handling tests for LAL class generator.
+ */
+class LALClassGeneratorBasicTest extends LALClassGeneratorTestBase {
+
+    // ==================== Minimal compile tests ====================
+
+    @Test
+    void compileMinimalFilter() throws Exception {
+        compileAndAssert("filter { sink {} }");
+    }
+
+    @Test
+    void compileJsonParserFilter() throws Exception {
+        compileAndAssert("filter { json {} sink {} }");
+    }
+
+    @Test
+    void compileJsonWithExtractor() throws Exception {
+        compileAndAssert(
+            "filter {\n"
+            + "  json {}\n"
+            + "  extractor {\n"
+            + "    service parsed.service as String\n"
+            + "  }\n"
+            + "  sink {}\n"
+            + "}");
+    }
+
+    @Test
+    void compileTextWithRegexp() throws Exception {
+        compileAndAssert(
+            "filter {\n"
+            + "  text {\n"
+            + "    regexp '(?<timestamp>\\\\d+) (?<level>\\\\w+) (?<msg>.*)'\n"
+            + "  }\n"
+            + "  sink {}\n"
+            + "}");
+    }
+
+    @Test
+    void compileSinkWithEnforcer() throws Exception {
+        compileAndAssert(
+            "filter {\n"
+            + "  json {}\n"
+            + "  sink {\n"
+            + "    enforcer {}\n"
+            + "  }\n"
+            + "}");
+    }
+
+    @Test
+    void generateSourceReturnsJavaCode() {
+        final String source = generator.generateSource(
+            "filter { json {} sink {} }");
+        assertNotNull(source);
+        assertTrue(source.contains("filterSpec.json(ctx)"));
+        assertTrue(source.contains("filterSpec.sink(ctx)"));
+    }
+
+    // ==================== Error handling ====================
+
+    @Test
+    void emptyScriptThrows() {
+        assertThrows(Exception.class, () -> generator.compile(""));
+    }
+
+    @Test
+    void missingFilterKeywordThrows() {
+        assertThrows(Exception.class, () -> generator.compile("json {}"));
+    }
+
+    @Test
+    void unclosedBraceThrows() {
+        assertThrows(Exception.class,
+            () -> generator.compile("filter { json {"));
+    }
+
+    @Test
+    void invalidStatementInFilterThrows() {
+        assertThrows(Exception.class,
+            () -> generator.compile("filter { invalid {} }"));
+    }
+}
diff --git 
a/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGeneratorConditionTest.java
 
b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGeneratorConditionTest.java
new file mode 100644
index 0000000000..b537a3e940
--- /dev/null
+++ 
b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGeneratorConditionTest.java
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.log.analyzer.v2.compiler;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Condition, safe navigation, tag function, if-block, and else-if chain tests.
+ */
+class LALClassGeneratorConditionTest extends LALClassGeneratorTestBase {
+
+    // ==================== tag() function in conditions ====================
+
+    @Test
+    void compileTagFunctionInCondition() throws Exception {
+        compileAndAssert(
+            "filter {\n"
+            + "  json {}\n"
+            + "  if (tag(\"LOG_KIND\") == \"SLOW_SQL\") {\n"
+            + "    sink {}\n"
+            + "  }\n"
+            + "}");
+    }
+
+    @Test
+    void generateSourceTagFunctionEmitsTagValue() {
+        final String source = generator.generateSource(
+            "filter {\n"
+            + "  if (tag(\"LOG_KIND\") == \"SLOW_SQL\") {\n"
+            + "    sink {}\n"
+            + "  }\n"
+            + "}");
+        assertTrue(source.contains("h.tagValue(\"LOG_KIND\")"),
+            "Expected tagValue call but got: " + source);
+        assertTrue(source.contains("SLOW_SQL"));
+    }
+
+    @Test
+    void compileTagFunctionNestedInExtractor() throws Exception {
+        compileAndAssert(
+            "filter {\n"
+            + "  json {}\n"
+            + "  extractor {\n"
+            + "    if (tag(\"LOG_KIND\") == \"NET_PROFILING_SAMPLED_TRACE\") 
{\n"
+            + "      service parsed.service as String\n"
+            + "    }\n"
+            + "  }\n"
+            + "  sink {}\n"
+            + "}");
+    }
+
+    // ==================== Safe navigation ====================
+
+    @Test
+    void compileSafeNavigationFieldAccess() throws Exception {
+        compileAndAssert(
+            "filter {\n"
+            + "  json {}\n"
+            + "  extractor {\n"
+            + "    service parsed?.response?.service as String\n"
+            + "  }\n"
+            + "  sink {}\n"
+            + "}");
+    }
+
+    @Test
+    void compileSafeNavigationMethodCalls() throws Exception {
+        compileAndAssert(
+            "filter {\n"
+            + "  json {}\n"
+            + "  extractor {\n"
+            + "    service parsed?.flags?.toString()?.trim() as String\n"
+            + "  }\n"
+            + "  sink {}\n"
+            + "}");
+    }
+
+    @Test
+    void generateSourceSafeNavMethodEmitsSpecificHelper() {
+        final String source = generator.generateSource(
+            "filter {\n"
+            + "  json {}\n"
+            + "  if (parsed?.flags?.toString()) {\n"
+            + "    sink {}\n"
+            + "  }\n"
+            + "}");
+        assertTrue(source.contains("h.toString("),
+            "Expected toString helper for safe nav method but got: " + source);
+        assertTrue(source.contains("h.isNotEmpty("),
+            "Expected isNotEmpty for ExprCondition but got: " + source);
+    }
+
+    // ==================== If blocks in extractor/sink ====================
+
+    @Test
+    void compileIfInsideExtractor() throws Exception {
+        compileAndAssert(
+            "filter {\n"
+            + "  json {}\n"
+            + "  extractor {\n"
+            + "    if (parsed?.status) {\n"
+            + "      tag 'http.status_code': parsed.status\n"
+            + "    }\n"
+            + "    tag 'response.flag': parsed.flags\n"
+            + "  }\n"
+            + "  sink {}\n"
+            + "}");
+    }
+
+    @Test
+    void compileIfInsideExtractorWithTagCondition() throws Exception {
+        compileAndAssert(
+            "filter {\n"
+            + "  json {}\n"
+            + "  extractor {\n"
+            + "    if (tag(\"LOG_KIND\") == \"NET_PROFILING\") {\n"
+            + "      service parsed.service as String\n"
+            + "      layer parsed.layer as String\n"
+            + "    }\n"
+            + "  }\n"
+            + "  sink {}\n"
+            + "}");
+    }
+
+    // ==================== Else-if chain ====================
+
+    @Test
+    void compileElseIfChain() throws Exception {
+        compileAndAssert(
+            "filter {\n"
+            + "  json {}\n"
+            + "  if (parsed.a) {\n"
+            + "    sink {}\n"
+            + "  } else if (parsed.b) {\n"
+            + "    sink {}\n"
+            + "  } else if (parsed.c) {\n"
+            + "    sink {}\n"
+            + "  } else {\n"
+            + "    sink {}\n"
+            + "  }\n"
+            + "}");
+    }
+
+    @Test
+    void generateSourceElseIfEmitsNestedBranches() {
+        final String source = generator.generateSource(
+            "filter {\n"
+            + "  json {}\n"
+            + "  if (parsed.a) {\n"
+            + "    sink {}\n"
+            + "  } else if (parsed.b) {\n"
+            + "    sink {}\n"
+            + "  } else {\n"
+            + "    sink {}\n"
+            + "  }\n"
+            + "}");
+        assertTrue(source.contains("else"),
+            "Expected else branch but got: " + source);
+        int ifCount = 0;
+        for (int i = 0; i < source.length() - 2; i++) {
+            if (source.substring(i, i + 3).equals("if ")) {
+                ifCount++;
+            }
+        }
+        assertTrue(ifCount >= 2,
+            "Expected at least 2 if-conditions for else-if chain but got "
+            + ifCount + " in: " + source);
+    }
+}
diff --git 
a/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGeneratorDefTest.java
 
b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGeneratorDefTest.java
new file mode 100644
index 0000000000..d9cdafb097
--- /dev/null
+++ 
b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGeneratorDefTest.java
@@ -0,0 +1,220 @@
+/*
+ * 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 org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for {@code def} local variable support with
+ * {@code toJson()}/{@code toJsonArray()}.
+ *
+ * <p>Focuses on JSON body parsing → def var → typed method chain.
+ * ALS-specific proto chain tests belong in the ALS receiver module.
+ */
+class LALClassGeneratorDefTest extends LALClassGeneratorTestBase {
+
+    // ==================== Source generation ====================
+
+    @Test
+    void generateSourceDefWithToJson() {
+        final String source = generator.generateSource(
+            "filter {\n"
+                + "  json {}\n"
+                + "  extractor {\n"
+                + "    def config = toJson(parsed.metadata)\n"
+                + "    tag 'env': config?.get(\"env\")?.getAsString()\n"
+                + "  }\n"
+                + "  sink {}\n"
+                + "}");
+        assertTrue(source.contains("com.google.gson.JsonObject _d0"),
+            "Expected JsonObject declaration but got:\n" + source);
+        assertTrue(source.contains("h.toJsonObject("),
+            "Expected h.toJsonObject() call but got:\n" + source);
+        assertTrue(source.contains(".get(\"env\")"),
+            "Expected .get(\"env\") call but got:\n" + source);
+        assertTrue(source.contains(".getAsString()"),
+            "Expected .getAsString() call but got:\n" + source);
+    }
+
+    @Test
+    void generateSourceDefWithToJsonArray() {
+        final String source = generator.generateSource(
+            "filter {\n"
+                + "  json {}\n"
+                + "  extractor {\n"
+                + "    def items = toJsonArray(parsed.tags)\n"
+                + "    tag 'first': items?.get(0)?.getAsString()\n"
+                + "  }\n"
+                + "  sink {}\n"
+                + "}");
+        assertTrue(source.contains("com.google.gson.JsonArray _d0"),
+            "Expected JsonArray declaration but got:\n" + source);
+        assertTrue(source.contains("h.toJsonArray("),
+            "Expected h.toJsonArray() call but got:\n" + source);
+        assertTrue(source.contains(".get(0)"),
+            "Expected .get(0) call but got:\n" + source);
+    }
+
+    @Test
+    void generateSourceDefWithCondition() {
+        final String source = generator.generateSource(
+            "filter {\n"
+                + "  json {}\n"
+                + "  extractor {\n"
+                + "    def config = toJson(parsed.metadata)\n"
+                + "    if (config?.has(\"env\")) {\n"
+                + "      tag 'env': config?.get(\"env\")?.getAsString()\n"
+                + "    }\n"
+                + "  }\n"
+                + "  sink {}\n"
+                + "}");
+        assertTrue(source.contains("_d0 == null") || source.contains("_d0 != 
null"),
+            "Expected null check on _d0 but got:\n" + source);
+        assertTrue(source.contains(".has(\"env\")"),
+            "Expected .has(\"env\") call but got:\n" + source);
+    }
+
+    // ==================== Compilation with class output ====================
+
+    @Test
+    void compileDefWithNestedJsonAccess() throws Exception {
+        compileAndAssert(
+            "filter {\n"
+                + "  json {}\n"
+                + "  extractor {\n"
+                + "    def config = toJson(parsed.metadata)\n"
+                + "    tag 'env': config?.get(\"env\")?.getAsString()\n"
+                + "    tag 'region': config?.getAsJsonObject(\"location\")"
+                + "?.get(\"region\")?.getAsString()\n"
+                + "  }\n"
+                + "  sink {}\n"
+                + "}");
+    }
+
+    @Test
+    void compileDefWithToJsonArrayAndIndex() throws Exception {
+        compileAndAssert(
+            "filter {\n"
+                + "  json {}\n"
+                + "  extractor {\n"
+                + "    def items = toJsonArray(parsed.tags)\n"
+                + "    if (items?.size() > 0) {\n"
+                + "      tag 'first': items?.get(0)?.getAsString()\n"
+                + "    }\n"
+                + "  }\n"
+                + "  sink {}\n"
+                + "}");
+    }
+
+    @Test
+    void compileMultipleDefs() throws Exception {
+        compileAndAssert(
+            "filter {\n"
+                + "  json {}\n"
+                + "  extractor {\n"
+                + "    def config = toJson(parsed.metadata)\n"
+                + "    def roles = toJsonArray(parsed.roles)\n"
+                + "    tag 'env': config?.get(\"env\")?.getAsString()\n"
+                + "    if (roles?.size() > 0) {\n"
+                + "      tag 'role': roles?.get(0)?.getAsString()\n"
+                + "    }\n"
+                + "  }\n"
+                + "  sink {}\n"
+                + "}");
+    }
+
+    @Test
+    void compileDefWithConditionGuard() throws Exception {
+        compileAndAssert(
+            "filter {\n"
+                + "  json {}\n"
+                + "  extractor {\n"
+                + "    def config = toJson(parsed.metadata)\n"
+                + "    if (config?.has(\"payload\")) {\n"
+                + "      tag 'name': config?.getAsJsonObject(\"payload\")"
+                + "?.get(\"name\")?.getAsString()\n"
+                + "      tag 'iss': config?.getAsJsonObject(\"payload\")"
+                + "?.get(\"iss\")?.getAsString()\n"
+                + "    }\n"
+                + "  }\n"
+                + "  sink {}\n"
+                + "}");
+    }
+
+    // ==================== Type cast on def ====================
+
+    @Test
+    void generateSourceDefWithStringCast() {
+        final String source = generator.generateSource(
+            "filter {\n"
+                + "  json {}\n"
+                + "  extractor {\n"
+                + "    def svc = parsed.service as String\n"
+                + "    tag 'svc': svc\n"
+                + "  }\n"
+                + "  sink {}\n"
+                + "}");
+        assertTrue(source.contains("java.lang.String _d0"),
+            "Expected String declaration but got:\n" + source);
+        assertTrue(source.contains("(java.lang.String)"),
+            "Expected String cast but got:\n" + source);
+    }
+
+    @Test
+    void compileDefWithQualifiedNameCast() throws Exception {
+        // With inputType, parsed?.commonProperties returns AccessLogCommon.
+        // The cast narrows parsed?.metadata to Metadata (which is already the
+        // correct inferred type here, but demonstrates FQCN cast syntax).
+        // A more realistic use: narrowing an Object-typed return to a known
+        // subclass when the compiler can't infer the concrete type.
+        generator.setInputType(
+            io.envoyproxy.envoy.data.accesslog.v3.HTTPAccessLogEntry.class);
+        compileAndAssert(
+            "filter {\n"
+                + "  extractor {\n"
+                + "    def common = parsed?.commonProperties"
+                + " as io.envoyproxy.envoy.data.accesslog.v3.AccessLogCommon\n"
+                + "    tag 'cluster': common?.upstreamCluster\n"
+                + "  }\n"
+                + "  sink {}\n"
+                + "}");
+    }
+
+    @Test
+    void generateSourceDefWithQualifiedNameCast() {
+        generator.setInputType(
+            io.envoyproxy.envoy.data.accesslog.v3.HTTPAccessLogEntry.class);
+        final String source = generator.generateSource(
+            "filter {\n"
+                + "  extractor {\n"
+                + "    def common = parsed?.commonProperties"
+                + " as io.envoyproxy.envoy.data.accesslog.v3.AccessLogCommon\n"
+                + "    tag 'cluster': common?.upstreamCluster\n"
+                + "  }\n"
+                + "  sink {}\n"
+                + "}");
+        assertTrue(source.contains(
+                "io.envoyproxy.envoy.data.accesslog.v3.AccessLogCommon _d0"),
+            "Expected AccessLogCommon declaration but got:\n" + source);
+        assertTrue(source.contains(
+                "(io.envoyproxy.envoy.data.accesslog.v3.AccessLogCommon)"),
+            "Expected AccessLogCommon cast but got:\n" + source);
+    }
+}
diff --git 
a/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGeneratorExtractorTest.java
 
b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGeneratorExtractorTest.java
new file mode 100644
index 0000000000..8fa30a6adc
--- /dev/null
+++ 
b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGeneratorExtractorTest.java
@@ -0,0 +1,269 @@
+/*
+ * 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 org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Extractor feature tests: ProcessRegistry, metrics, inputType, outputType,
+ * output field assignment, and LogData fallback.
+ */
+class LALClassGeneratorExtractorTest extends LALClassGeneratorTestBase {
+
+    // ==================== ProcessRegistry static calls ====================
+
+    @Test
+    void compileProcessRegistryCall() throws Exception {
+        compileAndAssert(
+            "filter {\n"
+            + "  json {}\n"
+            + "  extractor {\n"
+            + "    service ProcessRegistry.generateVirtualLocalProcess("
+            + "parsed.service as String, parsed.serviceInstance as String"
+            + ") as String\n"
+            + "  }\n"
+            + "  sink {}\n"
+            + "}");
+    }
+
+    @Test
+    void compileProcessRegistryWithThreeArgs() throws Exception {
+        compileAndAssert(
+            "filter {\n"
+            + "  json {}\n"
+            + "  extractor {\n"
+            + "    service ProcessRegistry.generateVirtualRemoteProcess("
+            + "parsed.service as String, parsed.serviceInstance as String, "
+            + "parsed.address as String) as String\n"
+            + "  }\n"
+            + "  sink {}\n"
+            + "}");
+    }
+
+    // ==================== Metrics block ====================
+
+    @Test
+    void compileMetricsBlock() throws Exception {
+        compileAndAssert(
+            "filter {\n"
+            + "  json {}\n"
+            + "  extractor {\n"
+            + "    metrics {\n"
+            + "      timestamp log.timestamp as Long\n"
+            + "      labels level: parsed.level, service: log.service\n"
+            + "      name \"nginx_error_log_count\"\n"
+            + "      value 1\n"
+            + "    }\n"
+            + "  }\n"
+            + "  sink {}\n"
+            + "}");
+    }
+
+    // ==================== Complex production-like rules ====================
+
+    @Test
+    void compileNginxAccessLogRule() throws Exception {
+        compileAndAssert(
+            "filter {\n"
+            + "  if (tag(\"LOG_KIND\") == \"NGINX_ACCESS_LOG\") {\n"
+            + "    text {\n"
+            + "      regexp '.+\"(?<request>.+)\"(?<status>\\\\d{3}).+'\n"
+            + "    }\n"
+            + "    extractor {\n"
+            + "      if (parsed.status) {\n"
+            + "        tag 'http.status_code': parsed.status\n"
+            + "      }\n"
+            + "    }\n"
+            + "    sink {}\n"
+            + "  }\n"
+            + "}");
+    }
+
+    @Test
+    void compileEnvoyAlsAbortRuleFailsWithoutInputType() {
+        assertThrows(IllegalArgumentException.class, () -> generator.compile(
+            "filter {\n"
+            + "  if (parsed?.response?.responseCode?.value as Integer < 400"
+            + " && 
!parsed?.commonProperties?.responseFlags?.toString()?.trim()) {\n"
+            + "    abort {}\n"
+            + "  }\n"
+            + "  extractor {\n"
+            + "    if (parsed?.response?.responseCode) {\n"
+            + "      tag 'status.code': 
parsed?.response?.responseCode?.value\n"
+            + "    }\n"
+            + "    tag 'response.flag': 
parsed?.commonProperties?.responseFlags\n"
+            + "  }\n"
+            + "  sink {}\n"
+            + "}"));
+    }
+
+    @Test
+    void compileNoParserFallsBackToLogDataProto() throws Exception {
+        final String dsl =
+            "filter {\n"
+            + "  extractor {\n"
+            + "    service parsed.service as String\n"
+            + "    instance parsed.serviceInstance as String\n"
+            + "  }\n"
+            + "  sink {}\n"
+            + "}";
+        final String source = generator.generateSource(dsl);
+        assertTrue(source.contains("h.ctx().log().getService()"),
+            "Expected h.ctx().log().getService() but got: " + source);
+        assertTrue(source.contains("h.ctx().log().getServiceInstance()"),
+            "Expected h.ctx().log().getServiceInstance() but got: " + source);
+        assertFalse(source.contains("_p"),
+            "Should NOT have _p variable for LogData fallback but got: " + 
source);
+        compileAndAssert(dsl);
+    }
+
+    @Test
+    void compileInputTypeGeneratesDirectGetterCalls() throws Exception {
+        generator.setInputType(
+            io.envoyproxy.envoy.data.accesslog.v3.HTTPAccessLogEntry.class);
+        final String dsl =
+            "filter {\n"
+            + "  if (parsed?.response?.responseCode?.value as Integer < 400"
+            + " && 
!parsed?.commonProperties?.responseFlags?.toString()?.trim()) {\n"
+            + "    abort {}\n"
+            + "  }\n"
+            + "  extractor {\n"
+            + "    if (parsed?.response?.responseCode) {\n"
+            + "      tag 'status.code': 
parsed?.response?.responseCode?.value\n"
+            + "    }\n"
+            + "    tag 'response.flag': 
parsed?.commonProperties?.responseFlags\n"
+            + "  }\n"
+            + "  sink {}\n"
+            + "}";
+        final String source = generator.generateSource(dsl);
+        final String fqcn =
+            "io.envoyproxy.envoy.data.accesslog.v3.HTTPAccessLogEntry";
+        assertTrue(source.contains(
+            fqcn + " _p = (" + fqcn + ") h.ctx().extraLog()"),
+            "Expected _p local variable for inputType cast but got: " + 
source);
+        assertTrue(source.contains("_p.getResponse()"),
+            "Expected _p.getResponse() via cached variable but got: " + 
source);
+        assertTrue(source.contains("_p.getCommonProperties()"),
+            "Expected _p.getCommonProperties() via cached variable but got: "
+            + source);
+        assertFalse(source.contains("getAt"),
+            "Should NOT contain getAt calls but got: " + source);
+        assertTrue(source.contains("_p == null ? null :"),
+            "Expected null checks for ?. safe navigation but got: " + source);
+        assertTrue(source.contains("_t0") && source.contains("_t1"),
+            "Expected _tN local variables for chain dedup but got: " + source);
+        assertTrue(source.contains(".getValue() < 400"),
+            "Expected direct primitive comparison without boxing but got: "
+            + source);
+        assertFalse(source.contains("h.toLong"),
+            "Should NOT use h.toLong for primitive int comparison but got: "
+            + source);
+        assertTrue(source.contains("_o.addTag(\"status.code\""),
+            "Expected _o.addTag(key, value) but got: " + source);
+        assertFalse(source.contains("singletonMap"),
+            "Should NOT use singletonMap for single tags but got: " + source);
+        compileAndAssert(dsl);
+    }
+
+    // ==================== Output field assignment ====================
+
+    @Test
+    void compileOutputFieldAssignment() {
+        generator.setOutputType(TestOutputType.class);
+        final String source = generator.generateSource(
+            "filter {\n"
+                + "  json {}\n"
+                + "  extractor {\n"
+                + "    statement parsed.statement as String\n"
+                + "    latency parsed.latency as Long\n"
+                + "  }\n"
+                + "  sink {}\n"
+                + "}");
+        assertTrue(source.contains(".setStatement("),
+            "Expected direct setStatement() call but got: " + source);
+        assertTrue(source.contains(".setLatency("),
+            "Expected direct setLatency() call but got: " + source);
+        assertTrue(source.contains("h.toStr("),
+            "Expected toStr() cast for String but got: " + source);
+        assertTrue(source.contains("h.toLong("),
+            "Expected toLong() cast for Long but got: " + source);
+        assertTrue(source.contains("h.ctx().output()"),
+            "Expected ctx.output() access but got: " + source);
+        assertTrue(source.contains("h.ctx().setOutput(new"),
+            "Expected output object creation but got: " + source);
+    }
+
+    @Test
+    void compileOutputFieldWithOutputTypeValidation() {
+        generator.setOutputType(TestOutputType.class);
+        final String source = generator.generateSource(
+            "filter {\n"
+                + "  json {}\n"
+                + "  extractor {\n"
+                + "    statement parsed.stmt as String\n"
+                + "  }\n"
+                + "  sink {}\n"
+                + "}");
+        assertTrue(source.contains(".setStatement("),
+            "Expected direct setStatement() call but got: " + source);
+    }
+
+    @Test
+    void compileOutputFieldWithInvalidSetter() {
+        generator.setOutputType(TestOutputType.class);
+        final Exception ex = assertThrows(RuntimeException.class, () ->
+            generator.generateSource(
+                "filter {\n"
+                    + "  json {}\n"
+                    + "  extractor {\n"
+                    + "    nonExistentField parsed.x as String\n"
+                    + "  }\n"
+                    + "  sink {}\n"
+                    + "}"));
+        assertTrue(ex.getMessage().contains("setNonExistentField"),
+            "Expected error about missing setter but got: " + ex.getMessage());
+    }
+
+    /**
+     * Test output type with custom fields (statement, latency).
+     */
+    public static class TestOutputType {
+        private String statement;
+        private long latency;
+
+        public void setStatement(final String statement) {
+            this.statement = statement;
+        }
+
+        public String getStatement() {
+            return statement;
+        }
+
+        public void setLatency(final long latency) {
+            this.latency = latency;
+        }
+
+        public long getLatency() {
+            return latency;
+        }
+    }
+}
diff --git 
a/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGeneratorSinkTest.java
 
b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGeneratorSinkTest.java
new file mode 100644
index 0000000000..15e8aff13e
--- /dev/null
+++ 
b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGeneratorSinkTest.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.log.analyzer.v2.compiler;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Sampler, rateLimit, and interpolated ID tests.
+ */
+class LALClassGeneratorSinkTest extends LALClassGeneratorTestBase {
+
+    @Test
+    void compileSamplerWithRateLimit() throws Exception {
+        compileAndAssert(
+            "filter {\n"
+            + "  json {}\n"
+            + "  sink {\n"
+            + "    sampler {\n"
+            + "      rateLimit('service:error') {\n"
+            + "        rpm 6000\n"
+            + "      }\n"
+            + "    }\n"
+            + "  }\n"
+            + "}");
+    }
+
+    @Test
+    void compileSamplerWithInterpolatedId() throws Exception {
+        compileAndAssert(
+            "filter {\n"
+            + "  json {}\n"
+            + "  sink {\n"
+            + "    sampler {\n"
+            + "      rateLimit(\"${log.service}:${parsed.code}\") {\n"
+            + "        rpm 6000\n"
+            + "      }\n"
+            + "    }\n"
+            + "  }\n"
+            + "}");
+    }
+
+    @Test
+    void parseInterpolatedIdParts() {
+        final java.util.List<LALScriptModel.InterpolationPart> parts =
+            LALScriptParser.parseInterpolation(
+                "${log.service}:${parsed.code}");
+        assertNotNull(parts);
+        assertEquals(3, parts.size());
+        assertFalse(parts.get(0).isLiteral());
+        assertTrue(parts.get(0).getExpression().isLogRef());
+        assertTrue(parts.get(1).isLiteral());
+        assertEquals(":", parts.get(1).getLiteral());
+        assertFalse(parts.get(2).isLiteral());
+        assertTrue(parts.get(2).getExpression().isParsedRef());
+    }
+
+    @Test
+    void compileSamplerWithSafeNavInterpolatedId() throws Exception {
+        compileAndAssert(
+            "filter {\n"
+            + "  json {}\n"
+            + "  sink {\n"
+            + "    sampler {\n"
+            + "      rateLimit(\"${log.service}:${parsed?.commonProperties"
+            + "?.responseFlags?.toString()}\") {\n"
+            + "        rpm 6000\n"
+            + "      }\n"
+            + "    }\n"
+            + "  }\n"
+            + "}");
+    }
+
+    @Test
+    void compileSamplerWithIfAndRateLimit() throws Exception {
+        compileAndAssert(
+            "filter {\n"
+            + "  json {}\n"
+            + "  sink {\n"
+            + "    sampler {\n"
+            + "      if (parsed?.error) {\n"
+            + "        rateLimit('svc:err') {\n"
+            + "          rpm 6000\n"
+            + "        }\n"
+            + "      } else {\n"
+            + "        rateLimit('svc:ok') {\n"
+            + "          rpm 3000\n"
+            + "        }\n"
+            + "      }\n"
+            + "    }\n"
+            + "  }\n"
+            + "}");
+    }
+}
diff --git 
a/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGeneratorTest.java
 
b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGeneratorTest.java
deleted file mode 100644
index 3bb9bbc325..0000000000
--- 
a/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGeneratorTest.java
+++ /dev/null
@@ -1,641 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.skywalking.oap.log.analyzer.v2.compiler;
-
-import javassist.ClassPool;
-import org.apache.skywalking.oap.log.analyzer.v2.dsl.LalExpression;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-class LALClassGeneratorTest {
-
-    private LALClassGenerator generator;
-
-    @BeforeEach
-    void setUp() {
-        generator = new LALClassGenerator(new ClassPool(true));
-    }
-
-    @Test
-    void compileMinimalFilter() throws Exception {
-        final LalExpression expr = generator.compile(
-            "filter { sink {} }");
-        assertNotNull(expr);
-    }
-
-    @Test
-    void compileJsonParserFilter() throws Exception {
-        final LalExpression expr = generator.compile(
-            "filter { json {} sink {} }");
-        assertNotNull(expr);
-    }
-
-    @Test
-    void compileJsonWithExtractor() throws Exception {
-        final LalExpression expr = generator.compile(
-            "filter {\n"
-            + "  json {}\n"
-            + "  extractor {\n"
-            + "    service parsed.service as String\n"
-            + "  }\n"
-            + "  sink {}\n"
-            + "}");
-        assertNotNull(expr);
-    }
-
-    @Test
-    void compileTextWithRegexp() throws Exception {
-        final LalExpression expr = generator.compile(
-            "filter {\n"
-            + "  text {\n"
-            + "    regexp '(?<timestamp>\\\\d+) (?<level>\\\\w+) (?<msg>.*)'\n"
-            + "  }\n"
-            + "  sink {}\n"
-            + "}");
-        assertNotNull(expr);
-    }
-
-    @Test
-    void compileSinkWithEnforcer() throws Exception {
-        final LalExpression expr = generator.compile(
-            "filter {\n"
-            + "  json {}\n"
-            + "  sink {\n"
-            + "    enforcer {}\n"
-            + "  }\n"
-            + "}");
-        assertNotNull(expr);
-    }
-
-    @Test
-    void generateSourceReturnsJavaCode() {
-        final String source = generator.generateSource(
-            "filter { json {} sink {} }");
-        assertNotNull(source);
-        org.junit.jupiter.api.Assertions.assertTrue(
-            source.contains("filterSpec.json(ctx)"));
-        org.junit.jupiter.api.Assertions.assertTrue(
-            source.contains("filterSpec.sink(ctx)"));
-    }
-
-    // ==================== Error handling tests ====================
-
-    @Test
-    void emptyScriptThrows() {
-        // Demo error: LAL script parsing failed: 1:0 mismatched input '<EOF>'
-        //   expecting 'filter'
-        assertThrows(Exception.class, () -> generator.compile(""));
-    }
-
-    @Test
-    void missingFilterKeywordThrows() {
-        // Demo error: LAL script parsing failed: 1:0 extraneous input 'json'
-        //   expecting 'filter'
-        assertThrows(Exception.class, () -> generator.compile("json {}"));
-    }
-
-    @Test
-    void unclosedBraceThrows() {
-        // Demo error: LAL script parsing failed: 1:15 mismatched input '<EOF>'
-        //   expecting '}'
-        assertThrows(Exception.class,
-            () -> generator.compile("filter { json {"));
-    }
-
-    @Test
-    void invalidStatementInFilterThrows() {
-        // Demo error: LAL script parsing failed: 1:9 extraneous input 
'invalid'
-        //   expecting {'text', 'json', 'yaml', 'extractor', 'sink', 'abort', 
'if', '}'}
-        assertThrows(Exception.class,
-            () -> generator.compile("filter { invalid {} }"));
-    }
-
-    // ==================== tag() function in conditions ====================
-
-    @Test
-    void compileTagFunctionInCondition() throws Exception {
-        final LalExpression expr = generator.compile(
-            "filter {\n"
-            + "  json {}\n"
-            + "  if (tag(\"LOG_KIND\") == \"SLOW_SQL\") {\n"
-            + "    sink {}\n"
-            + "  }\n"
-            + "}");
-        assertNotNull(expr);
-    }
-
-    @Test
-    void generateSourceTagFunctionEmitsTagValue() {
-        final String source = generator.generateSource(
-            "filter {\n"
-            + "  if (tag(\"LOG_KIND\") == \"SLOW_SQL\") {\n"
-            + "    sink {}\n"
-            + "  }\n"
-            + "}");
-        // Should use tagValue helper, not emit null
-        assertTrue(source.contains("h.tagValue(\"LOG_KIND\")"),
-            "Expected tagValue call but got: " + source);
-        assertTrue(source.contains("SLOW_SQL"));
-    }
-
-    @Test
-    void compileTagFunctionNestedInExtractor() throws Exception {
-        final LalExpression expr = generator.compile(
-            "filter {\n"
-            + "  json {}\n"
-            + "  extractor {\n"
-            + "    if (tag(\"LOG_KIND\") == \"NET_PROFILING_SAMPLED_TRACE\") 
{\n"
-            + "      service parsed.service as String\n"
-            + "    }\n"
-            + "  }\n"
-            + "  sink {}\n"
-            + "}");
-        assertNotNull(expr);
-    }
-
-    // ==================== Safe navigation ====================
-
-    @Test
-    void compileSafeNavigationFieldAccess() throws Exception {
-        final LalExpression expr = generator.compile(
-            "filter {\n"
-            + "  json {}\n"
-            + "  extractor {\n"
-            + "    service parsed?.response?.service as String\n"
-            + "  }\n"
-            + "  sink {}\n"
-            + "}");
-        assertNotNull(expr);
-    }
-
-    @Test
-    void compileSafeNavigationMethodCalls() throws Exception {
-        final LalExpression expr = generator.compile(
-            "filter {\n"
-            + "  json {}\n"
-            + "  extractor {\n"
-            + "    service parsed?.flags?.toString()?.trim() as String\n"
-            + "  }\n"
-            + "  sink {}\n"
-            + "}");
-        assertNotNull(expr);
-    }
-
-    @Test
-    void generateSourceSafeNavMethodEmitsSpecificHelper() {
-        final String source = generator.generateSource(
-            "filter {\n"
-            + "  json {}\n"
-            + "  if (parsed?.flags?.toString()) {\n"
-            + "    sink {}\n"
-            + "  }\n"
-            + "}");
-        // Safe method calls should emit specific helpers, not generic safeCall
-        assertTrue(source.contains("h.toString("),
-            "Expected toString helper for safe nav method but got: " + source);
-        assertTrue(source.contains("h.isNotEmpty("),
-            "Expected isNotEmpty for ExprCondition but got: " + source);
-    }
-
-    // ==================== ProcessRegistry static calls ====================
-
-    @Test
-    void compileProcessRegistryCall() throws Exception {
-        final LalExpression expr = generator.compile(
-            "filter {\n"
-            + "  json {}\n"
-            + "  extractor {\n"
-            + "    service ProcessRegistry.generateVirtualLocalProcess("
-            + "parsed.service as String, parsed.serviceInstance as String"
-            + ") as String\n"
-            + "  }\n"
-            + "  sink {}\n"
-            + "}");
-        assertNotNull(expr);
-    }
-
-    @Test
-    void compileProcessRegistryWithThreeArgs() throws Exception {
-        final LalExpression expr = generator.compile(
-            "filter {\n"
-            + "  json {}\n"
-            + "  extractor {\n"
-            + "    service ProcessRegistry.generateVirtualRemoteProcess("
-            + "parsed.service as String, parsed.serviceInstance as String, "
-            + "parsed.address as String) as String\n"
-            + "  }\n"
-            + "  sink {}\n"
-            + "}");
-        assertNotNull(expr);
-    }
-
-    // ==================== Metrics block ====================
-
-    @Test
-    void compileMetricsBlock() throws Exception {
-        final LalExpression expr = generator.compile(
-            "filter {\n"
-            + "  json {}\n"
-            + "  extractor {\n"
-            + "    metrics {\n"
-            + "      timestamp log.timestamp as Long\n"
-            + "      labels level: parsed.level, service: log.service\n"
-            + "      name \"nginx_error_log_count\"\n"
-            + "      value 1\n"
-            + "    }\n"
-            + "  }\n"
-            + "  sink {}\n"
-            + "}");
-        assertNotNull(expr);
-    }
-
-    // ==================== Sampler / rateLimit ====================
-
-    @Test
-    void compileSamplerWithRateLimit() throws Exception {
-        final LalExpression expr = generator.compile(
-            "filter {\n"
-            + "  json {}\n"
-            + "  sink {\n"
-            + "    sampler {\n"
-            + "      rateLimit('service:error') {\n"
-            + "        rpm 6000\n"
-            + "      }\n"
-            + "    }\n"
-            + "  }\n"
-            + "}");
-        assertNotNull(expr);
-    }
-
-    @Test
-    void compileSamplerWithInterpolatedId() throws Exception {
-        final LalExpression expr = generator.compile(
-            "filter {\n"
-            + "  json {}\n"
-            + "  sink {\n"
-            + "    sampler {\n"
-            + "      rateLimit(\"${log.service}:${parsed.code}\") {\n"
-            + "        rpm 6000\n"
-            + "      }\n"
-            + "    }\n"
-            + "  }\n"
-            + "}");
-        assertNotNull(expr);
-    }
-
-    @Test
-    void parseInterpolatedIdParts() {
-        // Verify the parser correctly splits interpolated strings
-        final java.util.List<LALScriptModel.InterpolationPart> parts =
-            LALScriptParser.parseInterpolation(
-                "${log.service}:${parsed.code}");
-        assertNotNull(parts);
-        // expr, literal ":", expr
-        assertEquals(3, parts.size());
-        assertFalse(parts.get(0).isLiteral());
-        assertTrue(parts.get(0).getExpression().isLogRef());
-        assertTrue(parts.get(1).isLiteral());
-        assertEquals(":", parts.get(1).getLiteral());
-        assertFalse(parts.get(2).isLiteral());
-        assertTrue(parts.get(2).getExpression().isParsedRef());
-    }
-
-    @Test
-    void compileSamplerWithSafeNavInterpolatedId() throws Exception {
-        final LalExpression expr = generator.compile(
-            "filter {\n"
-            + "  json {}\n"
-            + "  sink {\n"
-            + "    sampler {\n"
-            + "      
rateLimit(\"${log.service}:${parsed?.commonProperties?.responseFlags?.toString()}\")
 {\n"
-            + "        rpm 6000\n"
-            + "      }\n"
-            + "    }\n"
-            + "  }\n"
-            + "}");
-        assertNotNull(expr);
-    }
-
-    @Test
-    void compileSamplerWithIfAndRateLimit() throws Exception {
-        final LalExpression expr = generator.compile(
-            "filter {\n"
-            + "  json {}\n"
-            + "  sink {\n"
-            + "    sampler {\n"
-            + "      if (parsed?.error) {\n"
-            + "        rateLimit('svc:err') {\n"
-            + "          rpm 6000\n"
-            + "        }\n"
-            + "      } else {\n"
-            + "        rateLimit('svc:ok') {\n"
-            + "          rpm 3000\n"
-            + "        }\n"
-            + "      }\n"
-            + "    }\n"
-            + "  }\n"
-            + "}");
-        assertNotNull(expr);
-    }
-
-    // ==================== If blocks in extractor/sink ====================
-
-    @Test
-    void compileIfInsideExtractor() throws Exception {
-        final LalExpression expr = generator.compile(
-            "filter {\n"
-            + "  json {}\n"
-            + "  extractor {\n"
-            + "    if (parsed?.status) {\n"
-            + "      tag 'http.status_code': parsed.status\n"
-            + "    }\n"
-            + "    tag 'response.flag': parsed.flags\n"
-            + "  }\n"
-            + "  sink {}\n"
-            + "}");
-        assertNotNull(expr);
-    }
-
-    @Test
-    void compileIfInsideExtractorWithTagCondition() throws Exception {
-        final LalExpression expr = generator.compile(
-            "filter {\n"
-            + "  json {}\n"
-            + "  extractor {\n"
-            + "    if (tag(\"LOG_KIND\") == \"NET_PROFILING\") {\n"
-            + "      service parsed.service as String\n"
-            + "      layer parsed.layer as String\n"
-            + "    }\n"
-            + "  }\n"
-            + "  sink {}\n"
-            + "}");
-        assertNotNull(expr);
-    }
-
-    // ==================== Complex production-like rules ====================
-
-    @Test
-    void compileNginxAccessLogRule() throws Exception {
-        final LalExpression expr = generator.compile(
-            "filter {\n"
-            + "  if (tag(\"LOG_KIND\") == \"NGINX_ACCESS_LOG\") {\n"
-            + "    text {\n"
-            + "      regexp '.+\"(?<request>.+)\"(?<status>\\\\d{3}).+'\n"
-            + "    }\n"
-            + "    extractor {\n"
-            + "      if (parsed.status) {\n"
-            + "        tag 'http.status_code': parsed.status\n"
-            + "      }\n"
-            + "    }\n"
-            + "    sink {}\n"
-            + "  }\n"
-            + "}");
-        assertNotNull(expr);
-    }
-
-    @Test
-    void compileEnvoyAlsAbortRuleFailsWithoutInputType() {
-        // envoy-als pattern has no parser (json/yaml/text) — falls back to 
LogData
-        // but LogData.Builder doesn't have getResponse(), so compile fails
-        assertThrows(IllegalArgumentException.class, () -> generator.compile(
-            "filter {\n"
-            + "  if (parsed?.response?.responseCode?.value as Integer < 400"
-            + " && 
!parsed?.commonProperties?.responseFlags?.toString()?.trim()) {\n"
-            + "    abort {}\n"
-            + "  }\n"
-            + "  extractor {\n"
-            + "    if (parsed?.response?.responseCode) {\n"
-            + "      tag 'status.code': 
parsed?.response?.responseCode?.value\n"
-            + "    }\n"
-            + "    tag 'response.flag': 
parsed?.commonProperties?.responseFlags\n"
-            + "  }\n"
-            + "  sink {}\n"
-            + "}"));
-    }
-
-    @Test
-    void compileNoParserFallsBackToLogDataProto() throws Exception {
-        // No parser (json/yaml/text) and no inputType — should use 
LogData.Builder
-        final String dsl =
-            "filter {\n"
-            + "  extractor {\n"
-            + "    service parsed.service as String\n"
-            + "    instance parsed.serviceInstance as String\n"
-            + "  }\n"
-            + "  sink {}\n"
-            + "}";
-        final String source = generator.generateSource(dsl);
-        // Should generate getter chains on h.ctx().log()
-        assertTrue(source.contains("h.ctx().log().getService()"),
-            "Expected h.ctx().log().getService() but got: " + source);
-        assertTrue(source.contains("h.ctx().log().getServiceInstance()"),
-            "Expected h.ctx().log().getServiceInstance() but got: " + source);
-        // No _p variable (LogData doesn't need it)
-        assertFalse(source.contains("_p"),
-            "Should NOT have _p variable for LogData fallback but got: " + 
source);
-        // Verify it compiles
-        final LalExpression expr = generator.compile(dsl);
-        assertNotNull(expr);
-    }
-
-    @Test
-    void compileInputTypeGeneratesDirectGetterCalls() throws Exception {
-        generator.setInputType(
-            io.envoyproxy.envoy.data.accesslog.v3.HTTPAccessLogEntry.class);
-        final String dsl =
-            "filter {\n"
-            + "  if (parsed?.response?.responseCode?.value as Integer < 400"
-            + " && 
!parsed?.commonProperties?.responseFlags?.toString()?.trim()) {\n"
-            + "    abort {}\n"
-            + "  }\n"
-            + "  extractor {\n"
-            + "    if (parsed?.response?.responseCode) {\n"
-            + "      tag 'status.code': 
parsed?.response?.responseCode?.value\n"
-            + "    }\n"
-            + "    tag 'response.flag': 
parsed?.commonProperties?.responseFlags\n"
-            + "  }\n"
-            + "  sink {}\n"
-            + "}";
-        final String source = generator.generateSource(dsl);
-        final String fqcn =
-            "io.envoyproxy.envoy.data.accesslog.v3.HTTPAccessLogEntry";
-        // Proto field access uses _p local variable, not inline cast
-        assertTrue(source.contains(
-            fqcn + " _p = (" + fqcn + ") h.ctx().extraLog()"),
-            "Expected _p local variable for inputType cast but got: " + 
source);
-        // Intermediate fields cached in _tN local variables
-        assertTrue(source.contains("_p.getResponse()"),
-            "Expected _p.getResponse() via cached variable but got: " + 
source);
-        assertTrue(source.contains("_p.getCommonProperties()"),
-            "Expected _p.getCommonProperties() via cached variable but got: " 
+ source);
-        assertFalse(source.contains("getAt"),
-            "Should NOT contain getAt calls but got: " + source);
-        // Safe navigation: null checks with == null on local variables
-        assertTrue(source.contains("_p == null ? null :"),
-            "Expected null checks for ?. safe navigation but got: " + source);
-        // Dedup: _tN variables declared once and reused
-        assertTrue(source.contains("_t0") && source.contains("_t1"),
-            "Expected _tN local variables for chain dedup but got: " + source);
-        // Numeric comparison: direct primitive via _tN variable, no h.toLong()
-        assertTrue(source.contains(".getValue() < 400"),
-            "Expected direct primitive comparison without boxing but got: " + 
source);
-        assertFalse(source.contains("h.toLong"),
-            "Should NOT use h.toLong for primitive int comparison but got: " + 
source);
-        // Single-tag: uses addTag on output builder
-        assertTrue(source.contains("_o.addTag(\"status.code\""),
-            "Expected _o.addTag(key, value) but got: " + source);
-        assertFalse(source.contains("singletonMap"),
-            "Should NOT use singletonMap for single tags but got: " + source);
-
-        // Verify it compiles
-        final LalExpression expr = generator.compile(dsl);
-        assertNotNull(expr);
-    }
-
-    // ==================== Else-if chain ====================
-
-    @Test
-    void compileElseIfChain() throws Exception {
-        final LalExpression expr = generator.compile(
-            "filter {\n"
-            + "  json {}\n"
-            + "  if (parsed.a) {\n"
-            + "    sink {}\n"
-            + "  } else if (parsed.b) {\n"
-            + "    sink {}\n"
-            + "  } else if (parsed.c) {\n"
-            + "    sink {}\n"
-            + "  } else {\n"
-            + "    sink {}\n"
-            + "  }\n"
-            + "}");
-        assertNotNull(expr);
-    }
-
-    @Test
-    void generateSourceElseIfEmitsNestedBranches() {
-        final String source = generator.generateSource(
-            "filter {\n"
-            + "  json {}\n"
-            + "  if (parsed.a) {\n"
-            + "    sink {}\n"
-            + "  } else if (parsed.b) {\n"
-            + "    sink {}\n"
-            + "  } else {\n"
-            + "    sink {}\n"
-            + "  }\n"
-            + "}");
-        // The else-if should produce a nested if inside else
-        assertTrue(source.contains("else"),
-            "Expected else branch but got: " + source);
-        // Both condition branches should appear
-        int ifCount = 0;
-        for (int i = 0; i < source.length() - 2; i++) {
-            if (source.substring(i, i + 3).equals("if ")) {
-                ifCount++;
-            }
-        }
-        assertTrue(ifCount >= 2,
-            "Expected at least 2 if-conditions for else-if chain but got "
-            + ifCount + " in: " + source);
-    }
-
-    @Test
-    void compileOutputFieldAssignment() {
-        generator.setOutputType(TestOutputType.class);
-        final String source = generator.generateSource(
-            "filter {\n"
-                + "  json {}\n"
-                + "  extractor {\n"
-                + "    statement parsed.statement as String\n"
-                + "    latency parsed.latency as Long\n"
-                + "  }\n"
-                + "  sink {}\n"
-                + "}");
-        assertTrue(source.contains(".setStatement("),
-            "Expected direct setStatement() call but got: " + source);
-        assertTrue(source.contains(".setLatency("),
-            "Expected direct setLatency() call but got: " + source);
-        assertTrue(source.contains("h.toStr("),
-            "Expected toStr() cast for String but got: " + source);
-        assertTrue(source.contains("h.toLong("),
-            "Expected toLong() cast for Long but got: " + source);
-        assertTrue(source.contains("h.ctx().output()"),
-            "Expected ctx.output() access but got: " + source);
-        assertTrue(source.contains("h.ctx().setOutput(new"),
-            "Expected output object creation but got: " + source);
-    }
-
-    @Test
-    void compileOutputFieldWithOutputTypeValidation() {
-        generator.setOutputType(TestOutputType.class);
-        final String source = generator.generateSource(
-            "filter {\n"
-                + "  json {}\n"
-                + "  extractor {\n"
-                + "    statement parsed.stmt as String\n"
-                + "  }\n"
-                + "  sink {}\n"
-                + "}");
-        assertTrue(source.contains(".setStatement("),
-            "Expected direct setStatement() call but got: " + source);
-    }
-
-    @Test
-    void compileOutputFieldWithInvalidSetter() {
-        generator.setOutputType(TestOutputType.class);
-        final Exception ex = assertThrows(RuntimeException.class, () ->
-            generator.generateSource(
-                "filter {\n"
-                    + "  json {}\n"
-                    + "  extractor {\n"
-                    + "    nonExistentField parsed.x as String\n"
-                    + "  }\n"
-                    + "  sink {}\n"
-                    + "}"));
-        assertTrue(ex.getMessage().contains("setNonExistentField"),
-            "Expected error about missing setter but got: " + ex.getMessage());
-    }
-
-    public static class TestOutputType {
-        private String statement;
-        private long latency;
-
-        public void setStatement(final String statement) {
-            this.statement = statement;
-        }
-
-        public String getStatement() {
-            return statement;
-        }
-
-        public void setLatency(final long latency) {
-            this.latency = latency;
-        }
-
-        public long getLatency() {
-            return latency;
-        }
-    }
-}
diff --git 
a/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGeneratorTestBase.java
 
b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGeneratorTestBase.java
new file mode 100644
index 0000000000..dbb0357418
--- /dev/null
+++ 
b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGeneratorTestBase.java
@@ -0,0 +1,68 @@
+/*
+ * 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.File;
+import javassist.ClassPool;
+import org.apache.skywalking.oap.log.analyzer.v2.dsl.LalExpression;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestInfo;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * Shared base for LAL class generator tests.
+ *
+ * <p>Provides a fresh {@link LALClassGenerator} per test and dumps generated
+ * {@code .class} files to {@code target/lal-generated-classes/} for
+ * inspection with tools like {@code javap -c}.
+ *
+ * <p>Generated class names follow the pattern
+ * {@code {TestClassName}_{testMethodName}} for easy identification,
+ * e.g. {@code BasicTest_compileMinimalFilter}.
+ */
+abstract class LALClassGeneratorTestBase {
+
+    private static final File CLASS_OUTPUT_DIR =
+        new File("target/lal-generated-classes");
+
+    protected LALClassGenerator generator;
+
+    @BeforeEach
+    void setUp(final TestInfo testInfo) {
+        generator = new LALClassGenerator(new ClassPool(true));
+        generator.setClassOutputDir(CLASS_OUTPUT_DIR);
+        // Build hint from test class + method for readable .class file names
+        final String className = getClass().getSimpleName()
+            .replace("LALClassGenerator", "");
+        final String methodName = testInfo.getTestMethod()
+            .map(m -> m.getName()).orElse("unknown");
+        generator.setClassNameHint(className + "_" + methodName);
+    }
+
+    /**
+     * Compiles a LAL DSL string and asserts it produces a non-null expression.
+     * The generated {@code .class} file is written to
+     * {@code target/lal-generated-classes/} for inspection.
+     */
+    protected LalExpression compileAndAssert(final String dsl) throws 
Exception {
+        final LalExpression expr = generator.compile(dsl);
+        assertNotNull(expr);
+        return expr;
+    }
+}
diff --git 
a/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALExpressionExecutionTest.java
 
b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALExpressionExecutionTest.java
index 4a93674ef5..302727c37b 100644
--- 
a/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALExpressionExecutionTest.java
+++ 
b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALExpressionExecutionTest.java
@@ -254,6 +254,11 @@ class LALExpressionExecutionTest {
 
         expr.execute(filterSpec, ctx);
 
+        // The output builder (LogBuilder) holds extracted fields.
+        // For standard fields (service, instance, endpoint, layer),
+        // check the output builder first, falling back to ctx.log() proto.
+        final Object outputObj = ctx.output();
+
         // Assert expected values
         @SuppressWarnings("unchecked")
         final Map<String, Object> expect =
@@ -268,20 +273,27 @@ class LALExpressionExecutionTest {
 
             switch (key) {
                 case "service":
-                    assertEquals(expected, ctx.log().getService(),
+                    assertEquals(expected,
+                        getOutputOrProto(outputObj, "service",
+                            ctx.log().getService()),
                         ruleName + ": service mismatch");
                     break;
                 case "instance":
                     assertEquals(expected,
-                        ctx.log().getServiceInstance(),
+                        getOutputOrProto(outputObj, "serviceInstance",
+                            ctx.log().getServiceInstance()),
                         ruleName + ": serviceInstance mismatch");
                     break;
                 case "endpoint":
-                    assertEquals(expected, ctx.log().getEndpoint(),
+                    assertEquals(expected,
+                        getOutputOrProto(outputObj, "endpoint",
+                            ctx.log().getEndpoint()),
                         ruleName + ": endpoint mismatch");
                     break;
                 case "layer":
-                    assertEquals(expected, ctx.log().getLayer(),
+                    assertEquals(expected,
+                        getOutputOrProto(outputObj, "layer",
+                            ctx.log().getLayer()),
                         ruleName + ": layer mismatch");
                     break;
                 case "save":
@@ -318,6 +330,30 @@ class LALExpressionExecutionTest {
         }
     }
 
+    /**
+     * Reads a field from the output builder if present, otherwise falls back
+     * to the proto value. The output builder (e.g. LogBuilder) stores
+     * extracted fields that haven't been flushed to LogData proto yet.
+     */
+    private static String getOutputOrProto(final Object outputObj,
+                                            final String fieldName,
+                                            final String protoValue) {
+        if (outputObj == null) {
+            return protoValue;
+        }
+        try {
+            final Field f = outputObj.getClass().getDeclaredField(fieldName);
+            f.setAccessible(true);
+            final Object val = f.get(outputObj);
+            if (val != null) {
+                return val.toString();
+            }
+        } catch (NoSuchFieldException | IllegalAccessException ignored) {
+            // Field not on this output type — fall back to proto
+        }
+        return protoValue;
+    }
+
     // ==================== LogData builder ====================
 
     @SuppressWarnings("unchecked")
diff --git 
a/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALScriptParserTest.java
 
b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALScriptParserTest.java
index bc39b443a3..afead4c9e9 100644
--- 
a/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALScriptParserTest.java
+++ 
b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALScriptParserTest.java
@@ -497,4 +497,121 @@ class LALScriptParserTest {
         assertEquals("latency", latency.getFieldName());
         assertEquals("Long", latency.getCastType());
     }
+
+    // ==================== Def statement parsing ====================
+
+    @Test
+    void parseDefWithToJson() {
+        final LALScriptModel model = LALScriptParser.parse(
+            "filter {\n"
+                + "  extractor {\n"
+                + "    def metadata = 
toJson(parsed?.request?.requestHeaders?.get(\"x-metadata\"))\n"
+                + "  }\n"
+                + "  sink {}\n"
+                + "}");
+
+        final LALScriptModel.ExtractorBlock extractor =
+            (LALScriptModel.ExtractorBlock) model.getStatements().get(0);
+        assertEquals(1, extractor.getStatements().size());
+        final LALScriptModel.DefStatement def =
+            assertInstanceOf(LALScriptModel.DefStatement.class,
+                extractor.getStatements().get(0));
+        assertEquals("metadata", def.getVarName());
+        assertNotNull(def.getInitializer());
+        assertEquals("toJson", def.getInitializer().getFunctionCallName());
+    }
+
+    @Test
+    void parseDefWithToJsonArray() {
+        final LALScriptModel model = LALScriptParser.parse(
+            "filter {\n"
+                + "  extractor {\n"
+                + "    def items = 
toJsonArray(parsed?.response?.responseTrailers?.get(\"x-items\"))\n"
+                + "  }\n"
+                + "  sink {}\n"
+                + "}");
+
+        final LALScriptModel.ExtractorBlock extractor =
+            (LALScriptModel.ExtractorBlock) model.getStatements().get(0);
+        final LALScriptModel.DefStatement def =
+            assertInstanceOf(LALScriptModel.DefStatement.class,
+                extractor.getStatements().get(0));
+        assertEquals("items", def.getVarName());
+        assertEquals("toJsonArray", 
def.getInitializer().getFunctionCallName());
+    }
+
+    @Test
+    void parseDefWithSimpleValueAccess() {
+        final LALScriptModel model = LALScriptParser.parse(
+            "filter {\n"
+                + "  extractor {\n"
+                + "    def code = parsed?.response?.responseCode\n"
+                + "  }\n"
+                + "  sink {}\n"
+                + "}");
+
+        final LALScriptModel.ExtractorBlock extractor =
+            (LALScriptModel.ExtractorBlock) model.getStatements().get(0);
+        final LALScriptModel.DefStatement def =
+            assertInstanceOf(LALScriptModel.DefStatement.class,
+                extractor.getStatements().get(0));
+        assertEquals("code", def.getVarName());
+        assertTrue(def.getInitializer().isParsedRef());
+    }
+
+    @Test
+    void parseDefAtFilterLevel() {
+        final LALScriptModel model = LALScriptParser.parse(
+            "filter {\n"
+                + "  json {}\n"
+                + "  def metadata = toJson(parsed.headers)\n"
+                + "  sink {}\n"
+                + "}");
+
+        assertEquals(3, model.getStatements().size());
+        final LALScriptModel.DefStatement def =
+            assertInstanceOf(LALScriptModel.DefStatement.class,
+                model.getStatements().get(1));
+        assertEquals("metadata", def.getVarName());
+    }
+
+    @Test
+    void parseDefWithPrimitiveCast() {
+        final LALScriptModel model = LALScriptParser.parse(
+            "filter {\n"
+                + "  extractor {\n"
+                + "    def svc = parsed?.service as String\n"
+                + "  }\n"
+                + "  sink {}\n"
+                + "}");
+
+        final LALScriptModel.ExtractorBlock extractor =
+            (LALScriptModel.ExtractorBlock) model.getStatements().get(0);
+        final LALScriptModel.DefStatement def =
+            assertInstanceOf(LALScriptModel.DefStatement.class,
+                extractor.getStatements().get(0));
+        assertEquals("svc", def.getVarName());
+        assertEquals("String", def.getCastType());
+    }
+
+    @Test
+    void parseDefWithQualifiedNameCast() {
+        final LALScriptModel model = LALScriptParser.parse(
+            "filter {\n"
+                + "  extractor {\n"
+                + "    def resp = parsed?.response"
+                + " as 
io.envoyproxy.envoy.data.accesslog.v3.HTTPResponseProperties\n"
+                + "  }\n"
+                + "  sink {}\n"
+                + "}");
+
+        final LALScriptModel.ExtractorBlock extractor =
+            (LALScriptModel.ExtractorBlock) model.getStatements().get(0);
+        final LALScriptModel.DefStatement def =
+            assertInstanceOf(LALScriptModel.DefStatement.class,
+                extractor.getStatements().get(0));
+        assertEquals("resp", def.getVarName());
+        
assertEquals("io.envoyproxy.envoy.data.accesslog.v3.HTTPResponseProperties",
+            def.getCastType());
+    }
 }
diff --git 
a/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/test/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/EnvoyAlsLalTest.java
 
b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/test/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/EnvoyAlsLalTest.java
new file mode 100644
index 0000000000..efe20e4f14
--- /dev/null
+++ 
b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/test/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/EnvoyAlsLalTest.java
@@ -0,0 +1,368 @@
+/*
+ * 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.receiver.envoy.persistence;
+
+import com.google.protobuf.Struct;
+import com.google.protobuf.UInt32Value;
+import com.google.protobuf.Value;
+import io.envoyproxy.envoy.config.core.v3.Metadata;
+import io.envoyproxy.envoy.data.accesslog.v3.AccessLogCommon;
+import io.envoyproxy.envoy.data.accesslog.v3.HTTPAccessLogEntry;
+import io.envoyproxy.envoy.data.accesslog.v3.HTTPResponseProperties;
+import java.io.File;
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.Optional;
+import javassist.ClassPool;
+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.ExecutionContext;
+import org.apache.skywalking.oap.log.analyzer.v2.dsl.LalExpression;
+import org.apache.skywalking.oap.log.analyzer.v2.dsl.spec.filter.FilterSpec;
+import org.apache.skywalking.oap.log.analyzer.v2.module.LogAnalyzerModule;
+import 
org.apache.skywalking.oap.log.analyzer.v2.provider.LogAnalyzerModuleConfig;
+import 
org.apache.skywalking.oap.log.analyzer.v2.provider.LogAnalyzerModuleProvider;
+import org.apache.skywalking.oap.server.core.CoreModule;
+import org.apache.skywalking.oap.server.core.config.ConfigService;
+import org.apache.skywalking.oap.server.core.config.NamingControl;
+import org.apache.skywalking.oap.server.core.config.group.EndpointNameGrouping;
+import org.apache.skywalking.oap.server.core.source.Log;
+import org.apache.skywalking.oap.server.core.source.SourceReceiver;
+import org.apache.skywalking.oap.server.library.module.ModuleManager;
+import org.apache.skywalking.oap.server.library.module.ModuleProviderHolder;
+import org.apache.skywalking.oap.server.library.module.ModuleServiceHolder;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * LAL compilation and execution tests for envoy ALS proto chains.
+ *
+ * <p>Tests {@code def} local variables with {@code toJson()} to extract
+ * JWT claims from envoy {@code filter_metadata} Struct, and verifies
+ * runtime tag extraction against real proto objects.
+ *
+ * <p>Generated {@code .class} files are dumped to
+ * {@code target/lal-generated-classes/} with names like
+ * {@code EnvoyAlsLalTest_defJwtFromFilterMetadata.class}.
+ */
+class EnvoyAlsLalTest {
+
+    private static final File CLASS_OUTPUT_DIR =
+        new File("target/lal-generated-classes");
+
+    private LALClassGenerator generator;
+    private NamingControl namingControl;
+
+    @BeforeEach
+    void setUp(final TestInfo testInfo) {
+        generator = new LALClassGenerator(new ClassPool(true));
+        generator.setInputType(HTTPAccessLogEntry.class);
+        generator.setOutputType(EnvoyAccessLogBuilder.class);
+        generator.setClassOutputDir(CLASS_OUTPUT_DIR);
+        final String methodName = testInfo.getTestMethod()
+            .map(m -> m.getName()).orElse("unknown");
+        generator.setClassNameHint("EnvoyAlsLalTest_" + methodName);
+        namingControl = new NamingControl(
+            512, 512, 512, new EndpointNameGrouping());
+    }
+
+    // ==================== def + toJson: JWT from filter_metadata ===========
+
+    /**
+     * LAL script under test:
+     * <pre>{@code
+     * filter {
+     *   extractor {
+     *     def jwt = toJson(parsed?.commonProperties?.metadata
+     *         ?.filterMetadataMap?.get("envoy.filters.http.jwt_authn"))
+     *     def payload = jwt?.getAsJsonObject("payload")
+     *     if (payload != null) {
+     *       tag 'email': payload?.get("email")?.getAsString()
+     *       tag 'group': payload?.get("group")?.getAsString()
+     *     }
+     *     tag 'status.code': parsed?.response?.responseCode?.value
+     *   }
+     *   sink {}
+     * }
+     * }</pre>
+     */
+    @Test
+    void defJwtFromFilterMetadata() throws Exception {
+        final String dsl =
+            "filter {\n"
+            + "  extractor {\n"
+            + "    def jwt = toJson(parsed?.commonProperties?.metadata"
+            + "?.filterMetadataMap?.get(\"envoy.filters.http.jwt_authn\"))\n"
+            + "    def payload = jwt?.getAsJsonObject(\"payload\")\n"
+            + "    if (payload != null) {\n"
+            + "      tag 'email': payload?.get(\"email\")?.getAsString()\n"
+            + "      tag 'group': payload?.get(\"group\")?.getAsString()\n"
+            + "    }\n"
+            + "    tag 'status.code': parsed?.response?.responseCode?.value\n"
+            + "  }\n"
+            + "  sink {}\n"
+            + "}";
+
+        final LalExpression expr = generator.compile(dsl);
+
+        final Struct jwtStruct = Struct.newBuilder()
+            .putFields("payload", Value.newBuilder()
+                .setStructValue(Struct.newBuilder()
+                    .putFields("email", Value.newBuilder()
+                        .setStringValue("[email protected]").build())
+                    .putFields("group", Value.newBuilder()
+                        .setStringValue("admin").build()))
+                .build())
+            .build();
+
+        final HTTPAccessLogEntry entry = HTTPAccessLogEntry.newBuilder()
+            .setResponse(HTTPResponseProperties.newBuilder()
+                .setResponseCode(UInt32Value.of(200)))
+            .setCommonProperties(AccessLogCommon.newBuilder()
+                .setMetadata(Metadata.newBuilder()
+                    .putFilterMetadata(
+                        "envoy.filters.http.jwt_authn", jwtStruct)))
+            .build();
+
+        final Log log = executeAndBuildLog(expr, entry,
+            "email", "group", "status.code");
+
+        assertTag(log, "email", "[email protected]");
+        assertTag(log, "group", "admin");
+        assertTag(log, "status.code", "200");
+    }
+
+    // ==================== def + toJson: missing metadata ===================
+
+    /**
+     * Same LAL script as {@link #defJwtFromFilterMetadata()}, but the
+     * input proto has no filter_metadata. Verifies null-safe navigation
+     * handles the missing Struct gracefully (no crash, no tags extracted).
+     * <pre>{@code
+     * filter {
+     *   extractor {
+     *     def jwt = toJson(parsed?.commonProperties?.metadata
+     *         ?.filterMetadataMap?.get("envoy.filters.http.jwt_authn"))
+     *     def payload = jwt?.getAsJsonObject("payload")
+     *     if (payload != null) {
+     *       tag 'email': payload?.get("email")?.getAsString()
+     *     }
+     *   }
+     *   sink {}
+     * }
+     * }</pre>
+     */
+    @Test
+    void defJwtMissingMetadata() throws Exception {
+        final String dsl =
+            "filter {\n"
+            + "  extractor {\n"
+            + "    def jwt = toJson(parsed?.commonProperties?.metadata"
+            + "?.filterMetadataMap?.get(\"envoy.filters.http.jwt_authn\"))\n"
+            + "    def payload = jwt?.getAsJsonObject(\"payload\")\n"
+            + "    if (payload != null) {\n"
+            + "      tag 'email': payload?.get(\"email\")?.getAsString()\n"
+            + "    }\n"
+            + "  }\n"
+            + "  sink {}\n"
+            + "}";
+
+        final LalExpression expr = generator.compile(dsl);
+
+        // Entry with NO filter_metadata — jwt should be null, no crash
+        final HTTPAccessLogEntry entry = HTTPAccessLogEntry.newBuilder()
+            .setResponse(HTTPResponseProperties.newBuilder()
+                .setResponseCode(UInt32Value.of(404)))
+            .build();
+
+        final ExecutionContext ctx = execute(expr, entry);
+        assertNotNull(ctx.output());
+    }
+
+    // ==================== def: multiple filter_metadata keys ===============
+
+    /**
+     * Two {@code def} variables extracting from different filter_metadata
+     * keys (jwt_authn for JWT claims, rbac for authorization result).
+     * Uses {@code def payload} to avoid duplicate
+     * {@code getAsJsonObject("payload")} calls.
+     * <pre>{@code
+     * filter {
+     *   extractor {
+     *     def jwt = toJson(parsed?.commonProperties?.metadata
+     *         ?.filterMetadataMap?.get("envoy.filters.http.jwt_authn"))
+     *     def rbac = toJson(parsed?.commonProperties?.metadata
+     *         ?.filterMetadataMap?.get("envoy.filters.http.rbac"))
+     *     def payload = jwt?.getAsJsonObject("payload")
+     *     if (payload != null) {
+     *       tag 'email': payload?.get("email")?.getAsString()
+     *     }
+     *     if (rbac?.has("shadow_engine_result")) {
+     *       tag 'rbac': rbac?.get("shadow_engine_result")?.getAsString()
+     *     }
+     *   }
+     *   sink {}
+     * }
+     * }</pre>
+     */
+    @Test
+    void defMultipleFilterMetadataKeys() throws Exception {
+        final String dsl =
+            "filter {\n"
+            + "  extractor {\n"
+            + "    def jwt = toJson(parsed?.commonProperties?.metadata"
+            + "?.filterMetadataMap?.get(\"envoy.filters.http.jwt_authn\"))\n"
+            + "    def rbac = toJson(parsed?.commonProperties?.metadata"
+            + "?.filterMetadataMap?.get(\"envoy.filters.http.rbac\"))\n"
+            + "    def payload = jwt?.getAsJsonObject(\"payload\")\n"
+            + "    if (payload != null) {\n"
+            + "      tag 'email': payload?.get(\"email\")?.getAsString()\n"
+            + "    }\n"
+            + "    if (rbac?.has(\"shadow_engine_result\")) {\n"
+            + "      tag 'rbac': rbac?.get(\"shadow_engine_result\")"
+            + "?.getAsString()\n"
+            + "    }\n"
+            + "  }\n"
+            + "  sink {}\n"
+            + "}";
+
+        final LalExpression expr = generator.compile(dsl);
+
+        final Struct jwtStruct = Struct.newBuilder()
+            .putFields("payload", Value.newBuilder()
+                .setStructValue(Struct.newBuilder()
+                    .putFields("email", Value.newBuilder()
+                        .setStringValue("[email protected]").build()))
+                .build())
+            .build();
+        final Struct rbacStruct = Struct.newBuilder()
+            .putFields("shadow_engine_result", Value.newBuilder()
+                .setStringValue("DENY").build())
+            .build();
+
+        final HTTPAccessLogEntry entry = HTTPAccessLogEntry.newBuilder()
+            .setResponse(HTTPResponseProperties.newBuilder()
+                .setResponseCode(UInt32Value.of(403)))
+            .setCommonProperties(AccessLogCommon.newBuilder()
+                .setMetadata(Metadata.newBuilder()
+                    .putFilterMetadata(
+                        "envoy.filters.http.jwt_authn", jwtStruct)
+                    .putFilterMetadata(
+                        "envoy.filters.http.rbac", rbacStruct)))
+            .build();
+
+        final Log log = executeAndBuildLog(expr, entry, "email", "rbac");
+
+        assertTag(log, "email", "[email protected]");
+        assertTag(log, "rbac", "DENY");
+    }
+
+    // ==================== Helpers ==========================================
+
+    private ExecutionContext execute(
+            final LalExpression expr,
+            final HTTPAccessLogEntry entry) throws Exception {
+        final LogData.Builder logData = LogData.newBuilder()
+            .setService("als-test-svc")
+            .setTimestamp(1609459200000L)
+            .setLayer("MESH");
+
+        final FilterSpec filterSpec = buildFilterSpec();
+        final ExecutionContext ctx = new ExecutionContext();
+        ctx.log(logData);
+        ctx.extraLog(entry);
+        expr.execute(filterSpec, ctx);
+        return ctx;
+    }
+
+    private Log executeAndBuildLog(
+            final LalExpression expr,
+            final HTTPAccessLogEntry entry,
+            final String... searchableTagKeys) throws Exception {
+        final ExecutionContext ctx = execute(expr, entry);
+
+        assertNotNull(ctx.output());
+        final EnvoyAccessLogBuilder output =
+            (EnvoyAccessLogBuilder) ctx.output();
+        
output.setSearchableTagKeys(java.util.Arrays.asList(searchableTagKeys));
+        output.init(ctx.log().build(), Optional.of(entry), namingControl);
+        return output.toLog();
+    }
+
+    private static void assertTag(final Log log,
+                                   final String key,
+                                   final String expectedValue) {
+        assertTrue(log.getTags().stream().anyMatch(
+                t -> key.equals(t.getKey())
+                    && expectedValue.equals(t.getValue())),
+            "Expected tag " + key + "=" + expectedValue
+                + ", got: " + log.getTags());
+    }
+
+    private FilterSpec buildFilterSpec() throws Exception {
+        final ModuleManager manager = mock(ModuleManager.class);
+        final Field f =
+            ModuleManager.class.getDeclaredField("isInPrepareStage");
+        f.setAccessible(true);
+        f.set(manager, false);
+
+        when(manager.find(anyString()))
+            .thenReturn(mock(ModuleProviderHolder.class));
+
+        final ModuleProviderHolder logHolder =
+            mock(ModuleProviderHolder.class);
+        final LogAnalyzerModuleProvider logProvider =
+            mock(LogAnalyzerModuleProvider.class);
+        when(logProvider.getMetricConverts())
+            .thenReturn(Collections.emptyList());
+        when(logHolder.provider()).thenReturn(logProvider);
+        when(manager.find(LogAnalyzerModule.NAME)).thenReturn(logHolder);
+
+        final ModuleProviderHolder coreHolder =
+            mock(ModuleProviderHolder.class);
+        final ModuleServiceHolder coreServices =
+            mock(ModuleServiceHolder.class);
+        when(coreHolder.provider()).thenReturn(coreServices);
+        when(manager.find(CoreModule.NAME)).thenReturn(coreHolder);
+
+        when(coreServices.getService(SourceReceiver.class))
+            .thenReturn(mock(SourceReceiver.class));
+        when(coreServices.getService(NamingControl.class))
+            .thenReturn(namingControl);
+        final ConfigService configService = mock(ConfigService.class);
+        when(configService.getSearchableLogsTags()).thenReturn("");
+        when(coreServices.getService(ConfigService.class))
+            .thenReturn(configService);
+
+        final FilterSpec filterSpec =
+            new FilterSpec(manager, new LogAnalyzerModuleConfig());
+        final Field slf =
+            FilterSpec.class.getDeclaredField("sinkListenerFactories");
+        slf.setAccessible(true);
+        slf.set(filterSpec, Collections.emptyList());
+
+        return filterSpec;
+    }
+}
diff --git 
a/test/script-cases/scripts/lal/test-lal/feature-cases/execution-def.input.data 
b/test/script-cases/scripts/lal/test-lal/feature-cases/execution-def.input.data
new file mode 100644
index 0000000000..d6b28f28fc
--- /dev/null
+++ 
b/test/script-cases/scripts/lal/test-lal/feature-cases/execution-def.input.data
@@ -0,0 +1,68 @@
+# 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.
+
+# Mock input data for execution-def.yaml rules.
+# Tests def local variable with toJson()/toJsonArray() on JSON body.
+# metadata field is a JSON string that toJson() parses into JsonObject.
+
+def-json-extract-service:
+  body-type: json
+  body: '{"metadata":"{\"service\":\"my-svc\"}"}'
+  expect:
+    save: true
+    abort: false
+    service: my-svc
+
+def-json-nested-access:
+  body-type: json
+  body: 
'{"metadata":"{\"app\":{\"name\":\"frontend\",\"endpoint\":\"/api/v2\"}}"}'
+  expect:
+    save: true
+    abort: false
+    service: frontend
+    endpoint: /api/v2
+
+def-json-with-condition:
+  body-type: json
+  body: '{"metadata":"{\"app\":{\"name\":\"backend\"}}"}'
+  expect:
+    save: true
+    abort: false
+    service: backend
+
+def-json-missing-field:
+  body-type: json
+  body: '{"other":"value"}'
+  expect:
+    save: true
+    abort: false
+    service: ""
+
+def-multiple-vars:
+  body-type: json
+  body: 
'{"metadata":"{\"service\":\"order-svc\"}","extra":"{\"endpoint\":\"/checkout\"}"}'
+  expect:
+    save: true
+    abort: false
+    service: order-svc
+    endpoint: /checkout
+
+def-null-var-safe-nav:
+  body-type: json
+  body: '{"other":"value"}'
+  expect:
+    save: true
+    abort: false
+    service: ""
diff --git 
a/test/script-cases/scripts/lal/test-lal/feature-cases/execution-def.yaml 
b/test/script-cases/scripts/lal/test-lal/feature-cases/execution-def.yaml
new file mode 100644
index 0000000000..2aee126062
--- /dev/null
+++ b/test/script-cases/scripts/lal/test-lal/feature-cases/execution-def.yaml
@@ -0,0 +1,99 @@
+# 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.
+
+# Execution tests for LAL 'def' local variable feature.
+# Tests toJson()/toJsonArray() on JSON body parsed values.
+# Uses standard field assignment (service/endpoint) for assertions,
+# since tag assertions require full sink pipeline.
+# Paired with execution-def.input.data for mock input and expected output.
+rules:
+  - name: def-json-extract-service
+    layer: GENERAL
+    dsl: |
+      filter {
+        json {}
+        extractor {
+          def config = toJson(parsed.metadata)
+          service config?.get("service")?.getAsString()
+        }
+        sink {}
+      }
+
+  - name: def-json-nested-access
+    layer: GENERAL
+    dsl: |
+      filter {
+        json {}
+        extractor {
+          def config = toJson(parsed.metadata)
+          service config?.getAsJsonObject("app")?.get("name")?.getAsString()
+          endpoint 
config?.getAsJsonObject("app")?.get("endpoint")?.getAsString()
+        }
+        sink {}
+      }
+
+  - name: def-json-with-condition
+    layer: GENERAL
+    dsl: |
+      filter {
+        json {}
+        extractor {
+          def config = toJson(parsed.metadata)
+          if (config?.has("app")) {
+            service config?.getAsJsonObject("app")?.get("name")?.getAsString()
+          }
+        }
+        sink {}
+      }
+
+  - name: def-json-missing-field
+    layer: GENERAL
+    dsl: |
+      filter {
+        json {}
+        extractor {
+          def config = toJson(parsed.nonexistent)
+          if (config?.has("key")) {
+            service config?.get("key")?.getAsString()
+          }
+        }
+        sink {}
+      }
+
+  - name: def-multiple-vars
+    layer: GENERAL
+    dsl: |
+      filter {
+        json {}
+        extractor {
+          def config = toJson(parsed.metadata)
+          def info = toJson(parsed.extra)
+          service config?.get("service")?.getAsString()
+          endpoint info?.get("endpoint")?.getAsString()
+        }
+        sink {}
+      }
+
+  - name: def-null-var-safe-nav
+    layer: GENERAL
+    dsl: |
+      filter {
+        json {}
+        extractor {
+          def config = toJson(parsed.nonexistent)
+          service config?.get("service")?.getAsString()
+        }
+        sink {}
+      }


Reply via email to