You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2026/03/13 01:29:23 UTC

(skywalking) branch master updated: LAL: introduce LogMetadata to decouple metadata from LogData in pipeline entry (#13742)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 2709c94ab3 LAL: introduce LogMetadata to decouple metadata from LogData in pipeline entry (#13742)
2709c94ab3 is described below

commit 2709c94ab3cc8997fd45c2ff38f3059cb0120074
Author: 吴晟 Wu Sheng <wu...@foxmail.com>
AuthorDate: Fri Mar 13 09:29:18 2026 +0800

    LAL: introduce LogMetadata to decouple metadata from LogData in pipeline entry (#13742)
    
      - Introduce LogMetadata POJO as uniform metadata carrier, replacing LogData for metadata fields (service, layer, timestamp, trace context)
      - ExecutionContext uses metadata() + input() instead of log() + extraLog(); removes LogData type awareness from runtime
      - Unify LogSinkListener.parse() to single 3-parameter signature (LogMetadata, Object, ExecutionContext)
      - Envoy ALS receivers pass typed proto directly as input — no longer build fake LogData
      - Separate test tool (LogTestQuery) from production sink via dryRun flag
      - LogMetadata.TraceContext defaults to EMPTY (matching protobuf's never-null semantics), preventing NPE on untraced logs
      - LogMetadataUtils.fromLogData(Builder) extracts fields directly without build()
      - tag("KEY") codegen is now input-type-aware: only intercepts for LogData.Builder input with single string literal arg; other cases fall through to normal type-oriented resolution
---
 .../listener/DatabaseSlowStatementBuilder.java     |   18 +-
 .../trace/parser/listener/SampledTraceBuilder.java |   22 +-
 oap-server/analyzer/log-analyzer/CLAUDE.md         |   27 +-
 .../log/analyzer/v2/compiler/LALBlockCodegen.java  | 1123 +++-----------------
 .../analyzer/v2/compiler/LALClassGenerator.java    |   17 +-
 .../log/analyzer/v2/compiler/LALCodegenHelper.java |   36 +-
 .../log/analyzer/v2/compiler/LALDefCodegen.java    |  360 +++++++
 .../log/analyzer/v2/compiler/LALValueCodegen.java  |  903 ++++++++++++++++
 .../analyzer/v2/compiler/rt/LalRuntimeHelper.java  |   15 +-
 .../skywalking/oap/log/analyzer/v2/dsl/DSL.java    |   10 +-
 .../oap/log/analyzer/v2/dsl/ExecutionContext.java  |   66 +-
 .../analyzer/v2/dsl/spec/filter/FilterSpec.java    |   67 +-
 .../v2/dsl/spec/parser/TextParserSpec.java         |    2 +-
 .../v2/provider/log/ILogAnalyzerService.java       |   14 +-
 .../log/analyzer/v2/provider/log/LogAnalyzer.java  |   29 +-
 .../v2/provider/log/LogAnalyzerServiceImpl.java    |    7 +-
 .../provider/log/listener/LogAnalysisListener.java |   12 +-
 .../provider/log/listener/LogFilterListener.java   |   10 +-
 .../v2/provider/log/listener/LogSinkListener.java  |   16 +-
 .../provider/log/listener/RecordSinkListener.java  |   20 +-
 .../provider/log/listener/TrafficSinkListener.java |   25 +-
 .../log/analyzer/v2/spi/LALSourceTypeProvider.java |    8 +-
 .../compiler/LALClassGeneratorExtractorTest.java   |   14 +-
 .../v2/compiler/LALExpressionExecutionTest.java    |   26 +-
 .../oap/server/core/source/LALOutputBuilder.java   |   11 +-
 .../oap/server/core/source/LogBuilder.java         |  101 +-
 .../oap/server/core/source/LogMetadata.java        |   52 +
 .../oap/server/core/source/LogMetadataUtils.java   |   71 ++
 .../agent/kafka/provider/handler/LogHandler.java   |    6 +-
 .../oap/query/graphql/resolver/LogTestQuery.java   |   18 +-
 .../envoy/persistence/EnvoyAccessLogBuilder.java   |   18 +-
 .../envoy/persistence/LogsPersistence.java         |   30 +-
 .../envoy/persistence/TCPLogsPersistence.java      |   30 +-
 .../persistence/EnvoyAccessLogBuilderTest.java     |   37 +-
 .../envoy/persistence/EnvoyAlsLalTest.java         |   17 +-
 .../otel/otlp/OpenTelemetryLogHandler.java         |   21 +-
 .../handler/grpc/LogReportServiceGrpcHandler.java  |    4 +-
 .../handler/rest/LogReportServiceHTTPHandler.java  |    7 +-
 .../oap/server/checker/lal/LalBenchmark.java       |   12 +-
 .../oap/server/checker/lal/LalComparisonTest.java  |   44 +-
 40 files changed, 1956 insertions(+), 1370 deletions(-)

diff --git a/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/DatabaseSlowStatementBuilder.java b/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/DatabaseSlowStatementBuilder.java
index db33d33e2f..1cd2c92f96 100644
--- a/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/DatabaseSlowStatementBuilder.java
+++ b/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/DatabaseSlowStatementBuilder.java
@@ -18,11 +18,9 @@
 
 package org.apache.skywalking.oap.server.analyzer.provider.trace.parser.listener;
 
-import java.util.Optional;
 import lombok.Getter;
 import lombok.Setter;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.skywalking.apm.network.logging.v3.LogData;
 import org.apache.skywalking.oap.server.core.CoreModule;
 import org.apache.skywalking.oap.server.core.analysis.DownSampling;
 import org.apache.skywalking.oap.server.core.analysis.IDManager;
@@ -31,6 +29,7 @@ import org.apache.skywalking.oap.server.core.analysis.TimeBucket;
 import org.apache.skywalking.oap.server.core.config.NamingControl;
 import org.apache.skywalking.oap.server.core.source.DatabaseSlowStatement;
 import org.apache.skywalking.oap.server.core.source.LALOutputBuilder;
+import org.apache.skywalking.oap.server.core.source.LogMetadata;
 import org.apache.skywalking.oap.server.core.source.SourceReceiver;
 import org.apache.skywalking.oap.server.library.module.ModuleManager;
 
@@ -70,7 +69,7 @@ public class DatabaseSlowStatementBuilder implements LALOutputBuilder {
     }
 
     /**
-     * Constructor for v1 (Groovy) path which doesn't use {@link #init}.
+     * Constructor for v1 (Groovy) path where NamingControl is resolved eagerly.
      */
     public DatabaseSlowStatementBuilder(final NamingControl namingControl) {
         NAMING_CONTROL = namingControl;
@@ -83,7 +82,7 @@ public class DatabaseSlowStatementBuilder implements LALOutputBuilder {
     }
 
     @Override
-    public void init(final LogData logData, final Optional<Object> extraLog,
+    public void init(final LogMetadata metadata, final Object input,
                      final ModuleManager moduleManager) {
         if (!INITIALIZED) {
             NAMING_CONTROL = moduleManager.find(CoreModule.NAME)
@@ -93,17 +92,20 @@ public class DatabaseSlowStatementBuilder implements LALOutputBuilder {
         }
         // Only populate fields not already set by the LAL extractor.
         if (this.serviceName == null) {
-            this.serviceName = logData.getService();
+            this.serviceName = metadata.getService();
         }
         if (this.traceId == null) {
-            this.traceId = logData.getTraceContext().getTraceId();
+            final LogMetadata.TraceContext tc = metadata.getTraceContext();
+            if (tc != null) {
+                this.traceId = tc.getTraceId();
+            }
         }
         if (this.timestamp == 0) {
-            this.timestamp = logData.getTimestamp();
+            this.timestamp = metadata.getTimestamp();
         }
         if (this.timeBucket == 0) {
             this.timeBucket = TimeBucket.getTimeBucket(
-                this.timestamp > 0 ? this.timestamp : logData.getTimestamp(),
+                this.timestamp > 0 ? this.timestamp : metadata.getTimestamp(),
                 DownSampling.Second);
         }
     }
diff --git a/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/SampledTraceBuilder.java b/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/SampledTraceBuilder.java
index 1605a7ca10..5c7068cb74 100644
--- a/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/SampledTraceBuilder.java
+++ b/oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/SampledTraceBuilder.java
@@ -20,11 +20,9 @@ package org.apache.skywalking.oap.server.analyzer.provider.trace.parser.listener
 
 import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
-import java.util.Optional;
 import lombok.Getter;
 import lombok.Setter;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.skywalking.apm.network.logging.v3.LogData;
 import org.apache.skywalking.oap.server.core.analysis.DownSampling;
 import org.apache.skywalking.oap.server.core.analysis.IDManager;
 import org.apache.skywalking.oap.server.core.analysis.Layer;
@@ -40,6 +38,7 @@ import org.apache.skywalking.oap.server.core.source.DefaultScopeDefine;
 import org.apache.skywalking.oap.server.core.source.DetectPoint;
 import org.apache.skywalking.oap.server.core.source.ISource;
 import org.apache.skywalking.oap.server.core.source.LALOutputBuilder;
+import org.apache.skywalking.oap.server.core.source.LogMetadata;
 import org.apache.skywalking.oap.server.core.source.ProcessRelation;
 import org.apache.skywalking.oap.server.core.source.SourceReceiver;
 import org.apache.skywalking.oap.server.library.module.ModuleManager;
@@ -94,7 +93,7 @@ public class SampledTraceBuilder implements LALOutputBuilder {
     }
 
     /**
-     * Constructor for v1 (Groovy) path which doesn't use {@link #init}.
+     * Constructor for v1 (Groovy) path where NamingControl is resolved eagerly.
      */
     public SampledTraceBuilder(final NamingControl namingControl) {
         NAMING_CONTROL = namingControl;
@@ -107,7 +106,7 @@ public class SampledTraceBuilder implements LALOutputBuilder {
     }
 
     @Override
-    public void init(final LogData logData, final Optional<Object> extraLog,
+    public void init(final LogMetadata metadata, final Object input,
                      final ModuleManager moduleManager) {
         if (!INITIALIZED) {
             NAMING_CONTROL = moduleManager.find(CoreModule.NAME)
@@ -117,19 +116,22 @@ public class SampledTraceBuilder implements LALOutputBuilder {
         }
         // Only populate fields not already set by the LAL extractor.
         if (this.traceId == null) {
-            this.traceId = logData.getTraceContext().getTraceId();
+            final LogMetadata.TraceContext tc = metadata.getTraceContext();
+            if (tc != null) {
+                this.traceId = tc.getTraceId();
+            }
         }
         if (this.serviceName == null) {
-            this.serviceName = logData.getService();
+            this.serviceName = metadata.getService();
         }
         if (this.serviceInstanceName == null) {
-            this.serviceInstanceName = logData.getServiceInstance();
+            this.serviceInstanceName = metadata.getServiceInstance();
         }
-        if (this.layer == null && !logData.getLayer().isEmpty()) {
-            this.layer = logData.getLayer();
+        if (this.layer == null && metadata.getLayer() != null && !metadata.getLayer().isEmpty()) {
+            this.layer = metadata.getLayer();
         }
         if (this.timestamp == 0) {
-            this.timestamp = logData.getTimestamp();
+            this.timestamp = metadata.getTimestamp();
         }
     }
 
diff --git a/oap-server/analyzer/log-analyzer/CLAUDE.md b/oap-server/analyzer/log-analyzer/CLAUDE.md
index 3a0f6b0d13..2dbc40b904 100644
--- a/oap-server/analyzer/log-analyzer/CLAUDE.md
+++ b/oap-server/analyzer/log-analyzer/CLAUDE.md
@@ -41,7 +41,7 @@ oap-server/analyzer/log-analyzer/
 
   src/main/java/.../dsl/
     LalExpression.java                  — Functional interface: execute(FilterSpec, ExecutionContext)
-    ExecutionContext.java               — Per-log execution state (log, parsed, flags)
+    ExecutionContext.java               — Per-log execution state (metadata, input, parsed, flags)
     DSL.java                            — Wraps compiled expression + FilterSpec
     spec/filter/FilterSpec.java         — Top-level filter spec (all methods take ctx explicitly)
     spec/extractor/MetricExtractor.java   — Handles LAL metrics {} blocks (prepare/submit samples to MAL)
@@ -182,10 +182,11 @@ The generator detects the parser type from the AST at compile time and generates
 | JSON/YAML | `parsed.service` | `h.mapVal("service")` |
 | JSON/YAML nested | `parsed.a.b` | `h.mapVal("a", "b")` |
 | TEXT (regexp) | `parsed.level` | `h.group("level")` |
-| NONE + inputType | `parsed.response.code` | `((ExtraLogType) h.ctx().extraLog()).getResponse().getCode()` |
-| NONE + no inputType | `parsed.service` | `h.ctx().log().getService()` (LogData.Builder fallback) |
-| log fields | `log.service` | `h.ctx().log().getService()` |
-| log trace | `log.traceContext.traceId` | `h.ctx().log().getTraceContext().getTraceId()` |
+| NONE + inputType | `parsed.response.code` | `((InputType) h.ctx().input()).getResponse().getCode()` |
+| NONE + no inputType | `parsed.service` | `h.ctx().metadata().getService()` (LogMetadata fallback) |
+| log fields (metadata) | `log.service` | `h.ctx().metadata().getService()` |
+| log fields (LogData) | `log.body` | `((LogData.Builder) h.ctx().input()).getBody()` |
+| log trace | `log.traceContext.traceId` | `h.ctx().metadata().getTraceContext().getTraceId()` |
 | tags | `tag("KEY")` | `h.tagValue("KEY")` |
 
 ### inputType and LALSourceTypeProvider SPI
@@ -195,7 +196,7 @@ For LAL rules with no DSL parser (`json{}`/`yaml{}`/`text{}`), the compiler need
 1. **DSL parser** (`json{}`, `yaml{}`, `text{}`) — parser wins, inputType is ignored
 2. **Explicit `inputType`** in YAML rule config — FQCN string, resolved via `Class.forName()`
 3. **`LALSourceTypeProvider` SPI** — default inputType for a layer, discovered via `ServiceLoader`
-4. **`LogData.Builder` fallback** — if none of the above, `parsed.*` generates getter chains on `LogData.Builder` with compile-time reflection validation. Fields not found on `LogData.Builder` cause `IllegalArgumentException` at boot.
+4. **`LogMetadata` fallback** — if none of the above, `parsed.*` generates getter chains on `LogMetadata` with compile-time reflection validation. Fields not found on `LogMetadata` cause `IllegalArgumentException` at boot.
 
 The SPI interface is in `org.apache.skywalking.oap.log.analyzer.v2.spi.LALSourceTypeProvider`. Receiver plugins implement it and register in `META-INF/services/`. Example: `EnvoyHTTPLALSourceTypeProvider` registers `HTTPAccessLogEntry` for `Layer.MESH`.
 
@@ -238,7 +239,7 @@ If no setter is found, compilation fails with an `IllegalArgumentException` at b
 
 **Runtime dispatch**: `RecordSinkListener.parse()` reads the output object from
 `ExecutionContext.output()` (already populated by generated code), calls
-`init(logData, extraLog, moduleManager)` to populate standard fields and resolve
+`init(metadata, input, moduleManager)` to populate standard fields and resolve
 services (e.g., `NamingControl`, `ConfigService`) from `ModuleManager`, then `build()`
 dispatches via `complete(sourceReceiver)`. Each builder caches resolved services in
 static fields so `ModuleManager` lookups only happen once.
@@ -276,7 +277,7 @@ Instance-based helper created at the start of `execute()`, holds the `ExecutionC
 - `mapVal(key)`, `mapVal(k1, k2)`, `mapVal(k1, k2, k3)` — JSON/YAML map access
 - `group(name)` — text regexp named group
 - `tagValue(key)` — log tag lookup
-- `ctx()` — access to ExecutionContext (for `h.ctx().log()` proto getters)
+- `ctx()` — access to ExecutionContext (for `h.ctx().metadata()` and `h.ctx().log()` getters)
 
 **Type conversion:** `toStr()`, `toLong()`, `toInt()`, `toBool()`
 
@@ -284,11 +285,11 @@ Instance-based helper created at the start of `execute()`, holds the `ExecutionC
 
 **Safe navigation:** `toString()`, `trim()`
 
-## JSON/YAML LogData Field Population
+## JSON/YAML Metadata Field Population
 
-When `json{}` or `yaml{}` parses the log body, `FilterSpec` also adds LogData proto fields
+When `json{}` or `yaml{}` parses the log body, `FilterSpec` also adds `LogMetadata` fields
 (`service`, `serviceInstance`, `endpoint`, `layer`, `timestamp`) to the parsed map via
-`putIfAbsent`. Body-parsed values take priority; proto fields serve as fallback. This matches
+`putIfAbsent`. Body-parsed values take priority; metadata fields serve as fallback. This matches
 v1 Groovy `Binding.Parsed.getAt(key)` behavior where `parsed.service` falls back to
 `LogData.getService()` when the JSON body doesn't contain a `service` key.
 
@@ -342,7 +343,7 @@ rule-name:
 
 ### Principles
 
-1. **`body-type` determines parsing**: `json` → `json{}` block, `text` → `text{}` block, `none` → proto extraLog or raw LogData access.
+1. **`body-type` determines parsing**: `json` → `json{}` block, `text` → `text{}` block, `none` → typed proto input or LogMetadata fallback.
 2. **`extra-log` for proto types**: When rules access `parsed.*` on protobuf types (e.g., `HTTPAccessLogEntry`), provide `proto-class` and `proto-json`. The test harness parses via `JsonFormat`.
 3. **`expect` section is mandatory**: Every entry must have `expect` with at least `save` and `abort`.
 4. **Tag assertions**: `tag.KEY` in expect asserts extracted tag values (e.g., `tag.status.code: "500"`).
@@ -380,7 +381,7 @@ Test utilities from `org.apache.skywalking.oap.server.testing.dsl`:
 - `LalRuleLoader.loadAllRules(Path)` — loads all LAL rules with companion `.input.data` or `.data.yaml`
 - `LalLogDataBuilder.buildLogData(Map)` — builds `LogData.Builder` from test input map
 - `LalLogDataBuilder.buildSyntheticLogData(String)` — builds synthetic LogData from DSL string
-- `LalLogDataBuilder.buildExtraLog(Map)` — builds proto Message for extraLog from input map
+- `LalLogDataBuilder.buildExtraLog(Map)` — builds proto Message for typed input from input map
 - `DslRuleLoader.findScriptsDir(String...)` — resolves scripts directory from candidates
 - `DslRuleLoader.findRuleLine(String[], String, int)` — finds 1-based line number of rule in YAML
 
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 629edf7f4e..6862c15126 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
@@ -23,13 +23,46 @@ 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;
 
 /**
- * Static code-generation methods for LAL extractor, sink, condition, and
- * value-access blocks. Extracted from {@link LALClassGenerator} for
- * readability; all methods are stateless and take a
- * {@link LALClassGenerator.GenCtx} parameter for shared state.
+ * Code generation for LAL block-level structures: {@code extractor},
+ * {@code sink}, {@code sampler}, {@code metrics}, tag assignments, and
+ * output-field assignments.
+ *
+ * <p>Given a LAL script:
+ * <pre>{@code
+ * filter {
+ *   json {}
+ *   extractor {                          // → generateExtractorMethod
+ *     service parsed.service as String   //   → generateFieldToOutput
+ *     tag 'status.code': parsed.code     //   → generateTagAssignment
+ *     latency parsed.latency as Long     //   → generateOutputFieldAssignment
+ *     if (tag("LOG_KIND") == "SLOW") {   //   → generateIfBlockInExtractor
+ *       ...
+ *     }
+ *     metrics {                          //   → generateMetricsInline
+ *       name "log_count"
+ *       value 1
+ *     }
+ *   }
+ *   sink {                               // → generateSinkMethod
+ *     sampler {                          //   → generateSamplerInline
+ *       rateLimit("${log.service}") {    //     → generateRateLimitInline
+ *         rpm 6000
+ *       }
+ *     }
+ *   }
+ * }
+ * }</pre>
+ *
+ * <p>Each block becomes a private method in the generated class (e.g.
+ * {@code _extractor()}, {@code _sink()}). This class emits the method
+ * scaffolding (signature, local variable declarations, LVT entries) and
+ * walks the AST statements within each block.
+ *
+ * <p>Expression-level codegen (value access, conditions, type casts) is
+ * delegated to {@link LALValueCodegen}. {@code def} variable codegen is
+ * delegated to {@link LALDefCodegen}.
  */
 final class LALBlockCodegen {
 
@@ -41,8 +74,6 @@ final class LALBlockCodegen {
         METRIC_EXTRACTOR + "$SampleBuilder";
     private static final String H =
         "org.apache.skywalking.oap.log.analyzer.v2.compiler.rt.LalRuntimeHelper";
-    private static final String PROCESS_REGISTRY =
-        "org.apache.skywalking.oap.meter.analyzer.v2.dsl.registry.ProcessRegistry";
 
     // Built-in function registry for def variable type inference.
     // Maps DSL function name → [runtime helper method, return type].
@@ -59,6 +90,24 @@ final class LALBlockCodegen {
 
     // ==================== Extractor method generation ====================
 
+    /**
+     * Generates the {@code _extractor()} private method for the LAL
+     * {@code extractor { ... }} block.
+     *
+     * <p>LAL example:
+     * <pre>{@code
+     * extractor {
+     *   service parsed.service as String
+     *   tag 'status.code': parsed?.response?.responseCode?.value
+     *   metrics { name "log_count"; value 1 }
+     * }
+     * }</pre>
+     *
+     * <p>The generated method signature is:
+     * {@code private void _extractor(MetricExtractor _e, LalRuntimeHelper h)}.
+     * The call site in {@code execute()} guards it with
+     * {@code if (!ctx.shouldAbort())}.
+     */
     static void generateExtractorMethod(final StringBuilder sb,
                                          final LALScriptModel.ExtractorBlock block,
                                          final LALClassGenerator.GenCtx genCtx) {
@@ -87,7 +136,7 @@ final class LALBlockCodegen {
             if (genCtx.inputType != null) {
                 final String elTypeName = genCtx.inputType.getName();
                 body.append("  ").append(elTypeName).append(" _p = (")
-                    .append(elTypeName).append(") h.ctx().extraLog();\n");
+                    .append(elTypeName).append(") h.ctx().input();\n");
                 lvtVars.add(new String[]{"_p",
                     "L" + elTypeName.replace('.', '/') + ";"});
             }
@@ -128,6 +177,11 @@ final class LALBlockCodegen {
         sb.append("  }\n");
     }
 
+    /**
+     * Walks extractor statements and dispatches each to its codegen method:
+     * field assignments, tag assignments, if-blocks, metrics blocks,
+     * output-field assignments, and def statements.
+     */
     static void generateExtractorBody(
             final StringBuilder sb,
             final List<? extends LALScriptModel.FilterStatement> stmts,
@@ -147,7 +201,7 @@ final class LALBlockCodegen {
                 generateOutputFieldAssignment(
                     sb, (LALScriptModel.OutputFieldAssignment) stmt, genCtx);
             } else if (stmt instanceof LALScriptModel.DefStatement) {
-                generateDefStatement(
+                LALDefCodegen.generateDefStatement(
                     sb, (LALScriptModel.DefStatement) stmt, genCtx);
             }
         }
@@ -172,6 +226,15 @@ final class LALBlockCodegen {
         {"setTimestamp"},
     };
 
+    /**
+     * Generates a standard field assignment to the output builder.
+     *
+     * <p>LAL: {@code service parsed.service as String}
+     * <br>Generated: {@code _o.setServiceName(h.toStr(h.mapVal("service")))}
+     *
+     * <p>Standard fields: service, instance, endpoint, layer, traceId,
+     * segmentId, spanId, timestamp.
+     */
     private static void generateFieldToOutput(
             final StringBuilder sb,
             final LALScriptModel.FieldAssignment field,
@@ -198,16 +261,16 @@ final class LALBlockCodegen {
         if (field.getFormatPattern() != null) {
             // Format pattern provided in LAL script (e.g., timestamp ... , "yyyy/MM/dd HH:mm:ss")
             sb.append("h.parseTimestamp(");
-            generateCastedValueAccess(sb, field.getValue(), "String", genCtx);
+            LALValueCodegen.generateCastedValueAccess(sb, field.getValue(), "String", genCtx);
             sb.append(", \"")
               .append(LALCodegenHelper.escapeJava(field.getFormatPattern()))
               .append("\")");
         } else if (paramType.isEnum()) {
             sb.append(paramType.getName()).append(".valueOf(");
-            generateCastedValueAccess(sb, field.getValue(), "String", genCtx);
+            LALValueCodegen.generateCastedValueAccess(sb, field.getValue(), "String", genCtx);
             sb.append(")");
         } else {
-            generateCastedValueAccess(sb, field.getValue(), effectiveCast, genCtx);
+            LALValueCodegen.generateCastedValueAccess(sb, field.getValue(), effectiveCast, genCtx);
         }
         sb.append(");\n");
     }
@@ -217,7 +280,7 @@ final class LALBlockCodegen {
             final LALScriptModel.IfBlock ifBlock,
             final LALClassGenerator.GenCtx genCtx) {
         sb.append("  if (");
-        generateCondition(sb, ifBlock.getCondition(), genCtx);
+        LALValueCodegen.generateCondition(sb, ifBlock.getCondition(), genCtx);
         sb.append(") {\n");
         generateExtractorBody(sb, ifBlock.getThenBranch(), genCtx);
         sb.append("  }\n");
@@ -230,6 +293,23 @@ final class LALBlockCodegen {
 
     // ==================== Metrics inline ====================
 
+    /**
+     * Generates an inlined {@code metrics { ... }} block inside the extractor.
+     *
+     * <p>LAL example:
+     * <pre>{@code
+     * metrics {
+     *   name "log_count"
+     *   timestamp log.timestamp as Long
+     *   labels level: parsed.level, service: parsed.service
+     *   value 1
+     * }
+     * }</pre>
+     *
+     * <p>Generated code prepares a {@code SampleBuilder} via
+     * {@code _e.prepareMetrics()}, sets name/timestamp/labels/value, then
+     * calls {@code _e.submitMetrics()}.
+     */
     static void generateMetricsInline(
             final StringBuilder sb,
             final LALScriptModel.MetricsBlock block,
@@ -242,7 +322,7 @@ final class LALBlockCodegen {
         }
         if (block.getTimestampValue() != null) {
             sb.append("  _metrics.timestamp(");
-            generateCastedValueAccess(sb, block.getTimestampValue(),
+            LALValueCodegen.generateCastedValueAccess(sb, block.getTimestampValue(),
                 block.getTimestampCast(), genCtx);
             sb.append(");\n");
         }
@@ -252,7 +332,7 @@ final class LALBlockCodegen {
                     : block.getLabels().entrySet()) {
                 sb.append("    _labels.put(\"")
                     .append(LALCodegenHelper.escapeJava(entry.getKey())).append("\", ");
-                generateCastedValueAccess(sb, entry.getValue().getValue(),
+                LALValueCodegen.generateCastedValueAccess(sb, entry.getValue().getValue(),
                     entry.getValue().getCastType(), genCtx);
                 sb.append(");\n");
             }
@@ -262,18 +342,18 @@ final class LALBlockCodegen {
             sb.append("  _metrics.value(");
             if ("Long".equals(block.getValueCast())) {
                 sb.append("(double) h.toLong(");
-                generateValueAccess(sb, block.getValue(), genCtx);
+                LALValueCodegen.generateValueAccess(sb, block.getValue(), genCtx);
                 sb.append(")");
             } else if ("Integer".equals(block.getValueCast())) {
                 sb.append("(double) h.toInt(");
-                generateValueAccess(sb, block.getValue(), genCtx);
+                LALValueCodegen.generateValueAccess(sb, block.getValue(), genCtx);
                 sb.append(")");
             } else {
                 if (block.getValue().isNumberLiteral()) {
                     sb.append("(double) ").append(block.getValue().getSegments().get(0));
                 } else {
                     sb.append("((Number) ");
-                    generateValueAccess(sb, block.getValue(), genCtx);
+                    LALValueCodegen.generateValueAccess(sb, block.getValue(), genCtx);
                     sb.append(").doubleValue()");
                 }
             }
@@ -285,6 +365,12 @@ final class LALBlockCodegen {
 
     // ==================== Tag assignment ====================
 
+    /**
+     * Generates tag assignment statements in the extractor.
+     *
+     * <p>LAL: {@code tag 'status.code': parsed?.response?.responseCode?.value}
+     * <br>Generated: {@code _o.addTag("status.code", h.toStr(...))}
+     */
     static void generateTagAssignment(final StringBuilder sb,
                                        final LALScriptModel.TagAssignment tag,
                                        final LALClassGenerator.GenCtx genCtx) {
@@ -292,7 +378,7 @@ final class LALBlockCodegen {
                 : tag.getTags().entrySet()) {
             sb.append("  _o.addTag(\"")
               .append(LALCodegenHelper.escapeJava(entry.getKey())).append("\", ");
-            generateStringValueAccess(sb, entry.getValue().getValue(),
+            LALValueCodegen.generateStringValueAccess(sb, entry.getValue().getValue(),
                 entry.getValue().getCastType(), genCtx);
             sb.append(");\n");
         }
@@ -348,6 +434,17 @@ final class LALBlockCodegen {
         return false;
     }
 
+    /**
+     * Generates an output-field assignment for custom output types.
+     *
+     * <p>LAL: {@code latency parsed.latency as Long}
+     * (where {@code latency} is not a standard field but a field on the
+     * {@code outputType} class, e.g. {@code SampledTrace.setLatency(long)})
+     * <br>Generated: {@code _o.setLatency(h.toLong(h.mapVal("latency")))}
+     *
+     * <p>The setter is validated at compile time via reflection on the
+     * output type. If no matching setter exists, compilation fails.
+     */
     static void generateOutputFieldAssignment(
             final StringBuilder sb,
             final LALScriptModel.OutputFieldAssignment field,
@@ -380,10 +477,10 @@ final class LALBlockCodegen {
             // Auto-convert String to enum: EnumType.valueOf(stringValue.toUpperCase())
             // toUpperCase() handles case-insensitive matching (e.g., "slow" → "SLOW")
             sb.append(paramType.getName()).append(".valueOf(((String) ");
-            generateCastedValueAccess(sb, field.getValue(), "String", genCtx);
+            LALValueCodegen.generateCastedValueAccess(sb, field.getValue(), "String", genCtx);
             sb.append(").toUpperCase())");
         } else {
-            generateCastedValueAccess(sb, field.getValue(), effectiveCast, genCtx);
+            LALValueCodegen.generateCastedValueAccess(sb, field.getValue(), effectiveCast, genCtx);
         }
         sb.append(");\n");
     }
@@ -432,6 +529,26 @@ final class LALBlockCodegen {
 
     // ==================== Sink method generation ====================
 
+    /**
+     * Generates the {@code _sink()} private method for the LAL
+     * {@code sink { ... }} block.
+     *
+     * <p>LAL example:
+     * <pre>{@code
+     * sink {
+     *   sampler {
+     *     rateLimit("${log.service}:${parsed?.response?.responseCode}") {
+     *       rpm 6000
+     *     }
+     *   }
+     * }
+     * }</pre>
+     *
+     * <p>The generated method signature is:
+     * {@code private void _sink(FilterSpec _f, LalRuntimeHelper h)}.
+     * Sink statements include {@code enforcer()}, {@code dropper()},
+     * {@code sampler { ... }}, and if-blocks.
+     */
     static void generateSinkMethod(final StringBuilder sb,
                                     final LALScriptModel.SinkBlock sink,
                                     final LALClassGenerator.GenCtx genCtx) {
@@ -460,7 +577,7 @@ final class LALBlockCodegen {
             if (genCtx.inputType != null) {
                 final String elTypeName = genCtx.inputType.getName();
                 body.append("  ").append(elTypeName).append(" _p = (")
-                    .append(elTypeName).append(") h.ctx().extraLog();\n");
+                    .append(elTypeName).append(") h.ctx().input();\n");
                 lvtVars.add(new String[]{"_p",
                     "L" + elTypeName.replace('.', '/') + ";"});
             }
@@ -503,7 +620,7 @@ final class LALBlockCodegen {
             final LALScriptModel.IfBlock ifBlock,
             final LALClassGenerator.GenCtx genCtx) {
         sb.append("  if (");
-        generateCondition(sb, ifBlock.getCondition(), genCtx);
+        LALValueCodegen.generateCondition(sb, ifBlock.getCondition(), genCtx);
         sb.append(") {\n");
         generateSinkBody(sb, ifBlock.getThenBranch(), genCtx);
         sb.append("  }\n");
@@ -516,6 +633,9 @@ final class LALBlockCodegen {
 
     // ==================== Sampler/RateLimit inline ====================
 
+    /**
+     * Generates a {@code sampler { ... }} block inside the sink.
+     */
     static void generateSamplerInline(
             final StringBuilder sb,
             final LALScriptModel.SamplerBlock block,
@@ -542,7 +662,7 @@ final class LALBlockCodegen {
             final LALScriptModel.IfBlock ifBlock,
             final LALClassGenerator.GenCtx genCtx) {
         sb.append("  if (");
-        generateCondition(sb, ifBlock.getCondition(), genCtx);
+        LALValueCodegen.generateCondition(sb, ifBlock.getCondition(), genCtx);
         sb.append(") {\n");
         generateSamplerContentsFromFilterStmts(sb, ifBlock.getThenBranch(), genCtx);
         sb.append("  }\n");
@@ -567,6 +687,18 @@ final class LALBlockCodegen {
         }
     }
 
+    /**
+     * Generates a {@code rateLimit("id") { rpm N }} block.
+     *
+     * <p>The ID can be a plain string or an interpolated string:
+     * <pre>{@code
+     * rateLimit("${log.service}:${parsed?.response?.responseCode}") {
+     *   rpm 6000
+     * }
+     * }</pre>
+     *
+     * <p>Generated: {@code _f.sampler().rateLimit(h.ctx(), "" + ... , 6000)}
+     */
     static void generateRateLimitInline(
             final StringBuilder sb,
             final LALScriptModel.RateLimitBlock block,
@@ -581,7 +713,7 @@ final class LALBlockCodegen {
                       .append("\"");
                 } else {
                     sb.append("String.valueOf(");
-                    generateValueAccess(sb, part.getExpression(), genCtx);
+                    LALValueCodegen.generateValueAccess(sb, part.getExpression(), genCtx);
                     sb.append(")");
                 }
             }
@@ -590,945 +722,4 @@ final class LALBlockCodegen {
         }
         sb.append(", ").append(block.getRpm()).append(");\n");
     }
-
-    // ==================== Conditions ====================
-
-    static void generateCondition(final StringBuilder sb,
-                                   final LALScriptModel.Condition cond,
-                                   final LALClassGenerator.GenCtx genCtx) {
-        if (cond instanceof LALScriptModel.ComparisonCondition) {
-            final LALScriptModel.ComparisonCondition cc =
-                (LALScriptModel.ComparisonCondition) cond;
-            switch (cc.getOp()) {
-                case EQ:
-                    sb.append("java.util.Objects.equals(");
-                    generateValueAccessObj(sb, cc.getLeft(), cc.getLeftCast(), genCtx);
-                    sb.append(", ");
-                    generateConditionValue(sb, cc.getRight(), genCtx);
-                    sb.append(")");
-                    break;
-                case NEQ:
-                    sb.append("!java.util.Objects.equals(");
-                    generateValueAccessObj(sb, cc.getLeft(), cc.getLeftCast(), genCtx);
-                    sb.append(", ");
-                    generateConditionValue(sb, cc.getRight(), genCtx);
-                    sb.append(")");
-                    break;
-                case GT:
-                    generateNumericComparison(sb, cc, " > ", genCtx);
-                    break;
-                case LT:
-                    generateNumericComparison(sb, cc, " < ", genCtx);
-                    break;
-                case GTE:
-                    generateNumericComparison(sb, cc, " >= ", genCtx);
-                    break;
-                case LTE:
-                    generateNumericComparison(sb, cc, " <= ", genCtx);
-                    break;
-                default:
-                    break;
-            }
-        } else if (cond instanceof LALScriptModel.LogicalCondition) {
-            final LALScriptModel.LogicalCondition lc =
-                (LALScriptModel.LogicalCondition) cond;
-            sb.append("(");
-            generateCondition(sb, lc.getLeft(), genCtx);
-            sb.append(lc.getOp() == LALScriptModel.LogicalOp.AND
-                ? " && " : " || ");
-            generateCondition(sb, lc.getRight(), genCtx);
-            sb.append(")");
-        } else if (cond instanceof LALScriptModel.NotCondition) {
-            sb.append("!(");
-            generateCondition(sb,
-                ((LALScriptModel.NotCondition) cond).getInner(), genCtx);
-            sb.append(")");
-        } else if (cond instanceof LALScriptModel.ExprCondition) {
-            final String ct = ((LALScriptModel.ExprCondition) cond).getCastType();
-            final String method = "Boolean".equals(ct) || "boolean".equals(ct)
-                ? ".isTrue(" : ".isNotEmpty(";
-            sb.append("h").append(method);
-            generateValueAccessObj(sb,
-                ((LALScriptModel.ExprCondition) cond).getExpr(),
-                ct, genCtx);
-            sb.append(")");
-        }
-    }
-
-    static void generateNumericComparison(
-            final StringBuilder sb,
-            final LALScriptModel.ComparisonCondition cc,
-            final String op,
-            final LALClassGenerator.GenCtx genCtx) {
-        // Generate left side into buffer to inspect resolved type
-        final StringBuilder leftBuf = new StringBuilder();
-        generateValueAccessObj(leftBuf, cc.getLeft(), null, genCtx);
-
-        final boolean primitiveNumeric = genCtx.lastResolvedType != null
-            && (genCtx.lastResolvedType == int.class
-                || genCtx.lastResolvedType == long.class);
-
-        if (primitiveNumeric && genCtx.lastRawChain != null) {
-            // Direct primitive comparison — no boxing, no h.toLong()
-            if (genCtx.lastNullChecks != null) {
-                sb.append("(").append(genCtx.lastNullChecks).append(" ? false : ")
-                  .append(genCtx.lastRawChain).append(op);
-                generateConditionValueNumeric(sb, cc.getRight(), genCtx);
-                sb.append(")");
-            } else {
-                sb.append(genCtx.lastRawChain).append(op);
-                generateConditionValueNumeric(sb, cc.getRight(), genCtx);
-            }
-        } else {
-            // Fallback: h.toLong() conversion
-            sb.append("h.toLong(").append(leftBuf).append(")").append(op);
-            generateConditionValueNumeric(sb, cc.getRight(), genCtx);
-        }
-    }
-
-    static void generateConditionValue(final StringBuilder sb,
-                                        final LALScriptModel.ConditionValue cv,
-                                        final LALClassGenerator.GenCtx genCtx) {
-        if (cv instanceof LALScriptModel.StringConditionValue) {
-            sb.append('"')
-              .append(LALCodegenHelper.escapeJava(
-                  ((LALScriptModel.StringConditionValue) cv).getValue()))
-              .append('"');
-        } else if (cv instanceof LALScriptModel.NumberConditionValue) {
-            final double val =
-                ((LALScriptModel.NumberConditionValue) cv).getValue();
-            sb.append("Long.valueOf(").append((long) val).append("L)");
-        } else if (cv instanceof LALScriptModel.BoolConditionValue) {
-            sb.append("Boolean.valueOf(")
-              .append(((LALScriptModel.BoolConditionValue) cv).isValue())
-              .append(")");
-        } else if (cv instanceof LALScriptModel.NullConditionValue) {
-            sb.append("null");
-        } else if (cv instanceof LALScriptModel.ValueAccessConditionValue) {
-            generateValueAccessObj(sb,
-                ((LALScriptModel.ValueAccessConditionValue) cv).getValue(),
-                null, genCtx);
-        }
-    }
-
-    static void generateConditionValueNumeric(
-            final StringBuilder sb,
-            final LALScriptModel.ConditionValue cv,
-            final LALClassGenerator.GenCtx genCtx) {
-        if (cv instanceof LALScriptModel.NumberConditionValue) {
-            sb.append((long) ((LALScriptModel.NumberConditionValue) cv)
-                .getValue()).append("L");
-        } else if (cv instanceof LALScriptModel.ValueAccessConditionValue) {
-            sb.append("h.toLong(");
-            generateValueAccessObj(sb,
-                ((LALScriptModel.ValueAccessConditionValue) cv).getValue(),
-                null, genCtx);
-            sb.append(")");
-        } else {
-            sb.append("0L");
-        }
-    }
-
-    // ==================== Value access ====================
-
-    static void generateCastedValueAccess(final StringBuilder sb,
-                                           final LALScriptModel.ValueAccess value,
-                                           final String castType,
-                                           final LALClassGenerator.GenCtx genCtx) {
-        if ("String".equals(castType)) {
-            sb.append("h.toStr(");
-            generateValueAccess(sb, value, genCtx);
-            sb.append(")");
-        } else if ("Long".equals(castType)) {
-            sb.append("h.toLong(");
-            generateValueAccess(sb, value, genCtx);
-            sb.append(")");
-        } else if ("Integer".equals(castType)) {
-            sb.append("h.toInt(");
-            generateValueAccess(sb, value, genCtx);
-            sb.append(")");
-        } else if ("Boolean".equals(castType)) {
-            sb.append("h.toBool(");
-            generateValueAccess(sb, value, genCtx);
-            sb.append(")");
-        } else {
-            generateValueAccess(sb, value, genCtx);
-        }
-    }
-
-    static void generateStringValueAccess(final StringBuilder sb,
-                                            final LALScriptModel.ValueAccess value,
-                                            final String castType,
-                                            final LALClassGenerator.GenCtx genCtx) {
-        if (castType == null || "String".equals(castType)) {
-            sb.append("h.toStr(");
-            generateValueAccess(sb, value, genCtx);
-            sb.append(")");
-        } else if ("Long".equals(castType)) {
-            sb.append("String.valueOf(h.toLong(");
-            generateValueAccess(sb, value, genCtx);
-            sb.append("))");
-        } else if ("Integer".equals(castType)) {
-            sb.append("String.valueOf(h.toInt(");
-            generateValueAccess(sb, value, genCtx);
-            sb.append("))");
-        } else if ("Boolean".equals(castType)) {
-            sb.append("String.valueOf(h.toBool(");
-            generateValueAccess(sb, value, genCtx);
-            sb.append("))");
-        } else {
-            sb.append("h.toStr(");
-            generateValueAccess(sb, value, genCtx);
-            sb.append(")");
-        }
-    }
-
-    static void generateValueAccessObj(final StringBuilder sb,
-                                        final LALScriptModel.ValueAccess value,
-                                        final String castType,
-                                        final LALClassGenerator.GenCtx genCtx) {
-        if ("String".equals(castType)) {
-            sb.append("h.toStr(");
-            generateValueAccess(sb, value, genCtx);
-            sb.append(")");
-        } else {
-            generateValueAccess(sb, value, genCtx);
-        }
-    }
-
-    static void generateValueAccess(final StringBuilder sb,
-                                     final LALScriptModel.ValueAccess value,
-                                     final LALClassGenerator.GenCtx genCtx) {
-        genCtx.clearExtraLogResult();
-
-        // Handle string concatenation (term1 + term2 + ...)
-        if (!value.getConcatParts().isEmpty()) {
-            sb.append("(\"\" + ");
-            for (int i = 0; i < value.getConcatParts().size(); i++) {
-                if (i > 0) {
-                    sb.append(" + ");
-                }
-                generateValueAccess(sb, value.getConcatParts().get(i), genCtx);
-            }
-            sb.append(")");
-            return;
-        }
-
-        // Handle parenthesized expression: (innerExpr as Type).chain...
-        if (value.getParenInner() != null) {
-            generateParenAccess(sb, value, genCtx);
-            return;
-        }
-
-        // Handle function call primaries (e.g., tag("LOG_KIND"))
-        if (value.getFunctionCallName() != null) {
-            if ("tag".equals(value.getFunctionCallName())
-                    && !value.getFunctionCallArgs().isEmpty()) {
-                sb.append("h.tagValue(\"");
-                final String key = value.getFunctionCallArgs().get(0)
-                    .getValue().getSegments().get(0);
-                sb.append(LALCodegenHelper.escapeJava(key)).append("\")");
-            } else {
-                sb.append("null");
-            }
-            return;
-        }
-
-        // Handle string/number literals
-        if (value.isStringLiteral() && value.getChain().isEmpty()) {
-            sb.append("\"").append(LALCodegenHelper.escapeJava(value.getSegments().get(0)))
-              .append("\"");
-            return;
-        }
-        if (value.isNumberLiteral() && value.getChain().isEmpty()) {
-            final String num = value.getSegments().get(0);
-            if (num.contains(".")) {
-                sb.append("Double.valueOf(").append(num).append(")");
-            } else {
-                sb.append("Integer.valueOf(").append(num).append(")");
-            }
-            return;
-        }
-
-        // Handle ProcessRegistry static calls
-        if (value.isProcessRegistryRef()) {
-            generateProcessRegistryCall(sb, value, genCtx);
-            return;
-        }
-
-        final List<LALScriptModel.ValueAccessSegment> chain = value.getChain();
-
-        // Handle log.X.Y direct proto getter chains
-        if (value.isLogRef()) {
-            generateLogAccess(sb, chain);
-            return;
-        }
-
-        // Handle parsed.X.Y with compile-time type analysis
-        if (value.isParsedRef()) {
-            generateParsedAccess(sb, chain, genCtx);
-            return;
-        }
-
-        // 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");
-            return;
-        }
-        // Treat as parsed ref
-        generateParsedAccess(sb, chain, genCtx);
-    }
-
-    // ==================== Parenthesized expression ====================
-
-    static void generateParenAccess(final StringBuilder sb,
-                                     final LALScriptModel.ValueAccess value,
-                                     final LALClassGenerator.GenCtx genCtx) {
-        // Generate the inner expression with cast
-        final String castType = value.getParenCast();
-        final StringBuilder inner = new StringBuilder();
-        if (castType != null) {
-            generateCastedValueAccess(inner, value.getParenInner(), castType, genCtx);
-        } else {
-            generateValueAccess(inner, value.getParenInner(), genCtx);
-        }
-
-        // Apply chain segments (methods, fields, index access)
-        String current = inner.toString();
-        for (final LALScriptModel.ValueAccessSegment seg : value.getChain()) {
-            if (seg instanceof LALScriptModel.MethodSegment) {
-                current = appendMethodSegment(current,
-                    (LALScriptModel.MethodSegment) seg, genCtx);
-            } else if (seg instanceof LALScriptModel.IndexSegment) {
-                current = current + "["
-                    + ((LALScriptModel.IndexSegment) seg).getIndex() + "]";
-            } else if (seg instanceof LALScriptModel.FieldSegment) {
-                final LALScriptModel.FieldSegment fs =
-                    (LALScriptModel.FieldSegment) seg;
-                if (fs.isSafeNav()) {
-                    current = "(" + current + " == null ? null : "
-                        + current + "." + fs.getName() + ")";
-                } else {
-                    current = current + "." + fs.getName();
-                }
-            }
-        }
-        sb.append(current);
-    }
-
-    // ==================== Log access (direct proto getters) ====================
-
-    static void generateLogAccess(final StringBuilder sb,
-                                   final List<LALScriptModel.ValueAccessSegment> chain) {
-        if (chain.isEmpty()) {
-            sb.append("h.ctx().log()");
-            return;
-        }
-
-        String current = "h.ctx().log()";
-        boolean needsBoxing = false;
-        String boxType = null;
-
-        for (int i = 0; i < chain.size(); i++) {
-            final LALScriptModel.ValueAccessSegment seg = chain.get(i);
-            if (seg instanceof LALScriptModel.FieldSegment) {
-                final String name = ((LALScriptModel.FieldSegment) seg).getName();
-                if (i == 0 && LALCodegenHelper.LOG_GETTERS.containsKey(name)) {
-                    if ("traceContext".equals(name)) {
-                        current = current + ".getTraceContext()";
-                    } else {
-                        current = current + "."
-                            + LALCodegenHelper.LOG_GETTERS.get(name) + "()";
-                        if (LALCodegenHelper.LONG_FIELDS.contains(name)) {
-                            needsBoxing = true;
-                            boxType = "Long";
-                        }
-                    }
-                } else if (i == 1 && current.endsWith(".getTraceContext()")
-                        && LALCodegenHelper.TRACE_CONTEXT_GETTERS.containsKey(name)) {
-                    current = current + "."
-                        + LALCodegenHelper.TRACE_CONTEXT_GETTERS.get(name) + "()";
-                    if (LALCodegenHelper.INT_FIELDS.contains(name)) {
-                        needsBoxing = true;
-                        boxType = "Integer";
-                    }
-                } else {
-                    throw new IllegalArgumentException(
-                        "Unknown log field: log." + name
-                            + ". Supported fields: "
-                            + LALCodegenHelper.LOG_GETTERS.keySet()
-                            + ", traceContext."
-                            + LALCodegenHelper.TRACE_CONTEXT_GETTERS.keySet());
-                }
-            } else if (seg instanceof LALScriptModel.MethodSegment) {
-                current = appendMethodSegment(current,
-                    (LALScriptModel.MethodSegment) seg, null);
-            }
-        }
-
-        if (needsBoxing) {
-            sb.append(boxType).append(".valueOf(").append(current).append(")");
-        } else {
-            sb.append(current);
-        }
-    }
-
-    // ==================== Parsed access (compile-time typed) ====================
-
-    static void generateParsedAccess(
-            final StringBuilder sb,
-            final List<LALScriptModel.ValueAccessSegment> chain,
-            final LALClassGenerator.GenCtx genCtx) {
-        if (chain.isEmpty()) {
-            sb.append("h.ctx().parsed()");
-            return;
-        }
-
-        // Collect leading field segments (stop at method/index)
-        final List<LALScriptModel.FieldSegment> fieldSegments = new ArrayList<>();
-        int methodStart = -1;
-        for (int i = 0; i < chain.size(); i++) {
-            final LALScriptModel.ValueAccessSegment seg = chain.get(i);
-            if (seg instanceof LALScriptModel.FieldSegment) {
-                fieldSegments.add((LALScriptModel.FieldSegment) seg);
-            } else {
-                methodStart = i;
-                break;
-            }
-        }
-
-        final List<String> fieldKeys = new ArrayList<>();
-        for (final LALScriptModel.FieldSegment fs : fieldSegments) {
-            fieldKeys.add(fs.getName());
-        }
-
-        String current;
-        switch (genCtx.parserType) {
-            case JSON:
-            case YAML:
-                current = LALCodegenHelper.generateMapValCall(fieldKeys);
-                break;
-            case TEXT:
-                if (!fieldKeys.isEmpty()) {
-                    current = "h.group(\""
-                        + LALCodegenHelper.escapeJava(fieldKeys.get(0)) + "\")";
-                } else {
-                    current = "h.ctx().parsed()";
-                }
-                break;
-            case NONE:
-                if (genCtx.inputType != null) {
-                    current = generateExtraLogAccess(fieldSegments, genCtx.inputType,
-                        "_p", true, genCtx);
-                } else {
-                    // No parser and no inputType — fall back to LogData proto
-                    current = generateExtraLogAccess(fieldSegments, LogData.Builder.class,
-                        "h.ctx().log()", false, genCtx);
-                }
-                break;
-            default:
-                current = "null";
-                break;
-        }
-
-        // Apply remaining method/index segments
-        if (methodStart >= 0) {
-            for (int i = methodStart; i < chain.size(); i++) {
-                final LALScriptModel.ValueAccessSegment seg = chain.get(i);
-                if (seg instanceof LALScriptModel.MethodSegment) {
-                    current = appendMethodSegment(current,
-                        (LALScriptModel.MethodSegment) seg, genCtx);
-                } else if (seg instanceof LALScriptModel.IndexSegment) {
-                    current = current + "["
-                        + ((LALScriptModel.IndexSegment) seg).getIndex() + "]";
-                } else if (seg instanceof LALScriptModel.FieldSegment) {
-                    current = current + "."
-                        + ((LALScriptModel.FieldSegment) seg).getName();
-                }
-            }
-        }
-
-        sb.append(current);
-    }
-
-    static String generateExtraLogAccess(
-            final List<LALScriptModel.FieldSegment> fieldSegments,
-            final Class<?> rootType,
-            final String rootExpr,
-            final boolean rootCanBeNull,
-            final LALClassGenerator.GenCtx genCtx) {
-        genCtx.usedProtoAccess = true;
-
-        if (fieldSegments.isEmpty()) {
-            return rootExpr;
-        }
-
-        final String typeName = rootType.getName();
-        final StringBuilder chainKey = new StringBuilder();
-        String prevVar = rootExpr;
-        Class<?> currentType = rootType;
-        boolean prevCanBeNull = rootCanBeNull;
-
-        for (int i = 0; i < fieldSegments.size(); i++) {
-            final LALScriptModel.FieldSegment seg = fieldSegments.get(i);
-            final String field = seg.getName();
-            final String getterName = "get" + Character.toUpperCase(field.charAt(0))
-                + field.substring(1);
-
-            final java.lang.reflect.Method getter;
-            try {
-                getter = currentType.getMethod(getterName);
-            } catch (NoSuchMethodException e) {
-                throw new IllegalArgumentException(
-                    "Cannot resolve getter " + currentType.getSimpleName()
-                        + "." + getterName + "() for type "
-                        + typeName + ". Check the field path in the LAL rule.");
-            }
-            final Class<?> returnType = getter.getReturnType();
-
-            if (chainKey.length() > 0) {
-                chainKey.append(".");
-            }
-            chainKey.append(field);
-            final String key = chainKey.toString();
-            final boolean isLast = i == fieldSegments.size() - 1;
-
-            // Primitive final segment: return inline expression, no variable
-            if (isLast && returnType.isPrimitive()) {
-                final String rawAccess = prevVar + "." + getterName + "()";
-                genCtx.lastResolvedType = returnType;
-                genCtx.lastRawChain = rawAccess;
-                final String boxName = LALCodegenHelper.boxTypeName(returnType);
-                if (seg.isSafeNav() && prevCanBeNull) {
-                    genCtx.lastNullChecks = prevVar + " == null";
-                    return "(" + prevVar + " == null ? null : "
-                        + boxName + ".valueOf(" + rawAccess + "))";
-                } else {
-                    genCtx.lastNullChecks = null;
-                    return boxName + ".valueOf(" + rawAccess + ")";
-                }
-            }
-
-            // Reuse existing variable (dedup)
-            final String existingVar = genCtx.protoVars.get(key);
-            if (existingVar != null) {
-                prevVar = existingVar;
-                currentType = returnType;
-                prevCanBeNull = true;
-                continue;
-            }
-
-            // Create new local variable declaration
-            final String newVar = "_t" + genCtx.protoVarCounter++;
-            final String returnTypeName = returnType.getName();
-            if (seg.isSafeNav() && prevCanBeNull) {
-                genCtx.protoVarDecls.append("  ").append(returnTypeName)
-                    .append(" ").append(newVar).append(" = ")
-                    .append(prevVar).append(" == null ? null : ")
-                    .append(prevVar).append(".").append(getterName).append("();\n");
-                prevCanBeNull = true;
-            } else {
-                genCtx.protoVarDecls.append("  ").append(returnTypeName)
-                    .append(" ").append(newVar).append(" = ")
-                    .append(prevVar).append(".").append(getterName).append("();\n");
-                prevCanBeNull = !returnType.isPrimitive();
-            }
-            genCtx.protoVars.put(key, newVar);
-            genCtx.protoLvtVars.add(new String[]{
-                newVar, "L" + returnTypeName.replace('.', '/') + ";"
-            });
-
-            prevVar = newVar;
-            currentType = returnType;
-        }
-
-        // Non-primitive final result — null checks are in declarations
-        genCtx.lastResolvedType = currentType;
-        genCtx.lastRawChain = prevVar;
-        genCtx.lastNullChecks = null;
-        return prevVar;
-    }
-
-    // ==================== 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 = "_def_" + varName;
-        final boolean alreadyDeclared = genCtx.localVars.containsKey(varName);
-
-        // 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 String funcName = init.getFunctionCallName();
-            final int argCount = init.getFunctionCallArgs().size();
-            if (argCount != 1) {
-                throw new IllegalArgumentException(
-                    funcName + "() requires exactly 1 argument, got " + argCount);
-            }
-            final Object[] info = BUILTIN_FUNCTIONS.get(funcName);
-            final String helperMethod = (String) info[0];
-            resolvedType = (Class<?>) info[1];
-
-            initExpr.append(helperMethod).append("(");
-            generateValueAccess(initExpr,
-                init.getFunctionCallArgs().get(0).getValue(), genCtx);
-            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) — skip if already declared
-        if (!alreadyDeclared) {
-            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(), genCtx);
-
-                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(
-            final StringBuilder sb,
-            final LALScriptModel.ValueAccess value,
-            final LALClassGenerator.GenCtx genCtx) {
-        final List<LALScriptModel.ValueAccessSegment> chain = value.getChain();
-        if (chain.isEmpty()) {
-            sb.append("null");
-            return;
-        }
-        final LALScriptModel.ValueAccessSegment seg = chain.get(0);
-        if (seg instanceof LALScriptModel.MethodSegment) {
-            final LALScriptModel.MethodSegment ms =
-                (LALScriptModel.MethodSegment) seg;
-            sb.append(PROCESS_REGISTRY).append(".")
-              .append(ms.getName()).append("(");
-            final List<LALScriptModel.FunctionArg> args = ms.getArguments();
-            for (int i = 0; i < args.size(); i++) {
-                if (i > 0) {
-                    sb.append(", ");
-                }
-                generateCastedValueAccess(sb,
-                    args.get(i).getValue(), args.get(i).getCastType(), genCtx);
-            }
-            sb.append(")");
-        } else {
-            sb.append("null");
-        }
-    }
-
-    // ==================== Utility methods ====================
-
-    static String appendMethodSegment(final String current,
-                                       final LALScriptModel.MethodSegment ms,
-                                       final LALClassGenerator.GenCtx genCtx) {
-        final String mn = ms.getName();
-        final String args = ms.getArguments().isEmpty()
-            ? "" : generateMethodArgs(ms.getArguments(), genCtx);
-        if (ms.isSafeNav()) {
-            // 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 + ")";
-            }
-            // General safe-nav: null guard with ternary
-            return "(" + current + " == null ? null : "
-                + current + "." + mn + "(" + args + "))";
-        } else {
-            return current + "." + mn + "(" + args + ")";
-        }
-    }
-
-    static String generateMethodArgs(
-            final List<LALScriptModel.FunctionArg> args,
-            final LALClassGenerator.GenCtx genCtx) {
-        final StringBuilder sb = new StringBuilder();
-        for (int i = 0; i < args.size(); i++) {
-            if (i > 0) {
-                sb.append(", ");
-            }
-            final LALScriptModel.FunctionArg arg = args.get(i);
-            final LALScriptModel.ValueAccess va = arg.getValue();
-            if (va.isStringLiteral()) {
-                sb.append("\"").append(LALCodegenHelper.escapeJava(
-                    va.getSegments().get(0))).append("\"");
-            } else if (va.isNumberLiteral()) {
-                sb.append(va.getSegments().get(0));
-            } else if (!va.getSegments().isEmpty()) {
-                final String text = va.getSegments().get(0);
-                if ("true".equals(text) || "false".equals(text)
-                        || "null".equals(text)) {
-                    // Boolean or null literal
-                    sb.append(text);
-                } else if (genCtx != null
-                        && genCtx.localVars.containsKey(text)) {
-                    // Local def variable reference
-                    sb.append(genCtx.localVars.get(text).javaVarName);
-                } else {
-                    throw new IllegalArgumentException(
-                        "Unknown identifier used as method argument: '" + text + "'");
-                }
-            } else {
-                sb.append("null");
-            }
-        }
-        return sb.toString();
-    }
 }
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGenerator.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGenerator.java
index 3a1cab3655..59854cc961 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
@@ -440,7 +440,13 @@ public final class LALClassGenerator {
         final Class<?> resolvedOutput = this.outputType != null
             ? this.outputType
             : org.apache.skywalking.oap.server.core.source.LogBuilder.class;
-        final GenCtx genCtx = new GenCtx(parserType, this.inputType, resolvedOutput);
+        // inputType is only meaningful for parser-less rules (NONE) where parsed.*
+        // generates direct proto getter calls.  When a parser is present (json/yaml/text),
+        // parsed.* reads from the parsed map and tag() reads from LogData.Builder tags,
+        // so inputType must be null to avoid mis-guarding codegen branches.
+        final Class<?> effectiveInputType =
+            parserType == ParserType.NONE ? this.inputType : null;
+        final GenCtx genCtx = new GenCtx(parserType, effectiveInputType, resolvedOutput);
 
         if (parserType == ParserType.NONE && this.inputType != null) {
             log.info("LAL rule has no parser — using inputType {} for "
@@ -543,7 +549,7 @@ public final class LALClassGenerator {
             if (genCtx.inputType != null) {
                 final String elTypeName = genCtx.inputType.getName();
                 sb.append("  ").append(elTypeName).append(" _p = (")
-                  .append(elTypeName).append(") h.ctx().extraLog();\n");
+                  .append(elTypeName).append(") h.ctx().input();\n");
             }
             sb.append(genCtx.protoVarDecls);
         }
@@ -589,7 +595,7 @@ public final class LALClassGenerator {
         } else if (stmt instanceof LALScriptModel.IfBlock) {
             generateTopLevelIfBlock(sb, (LALScriptModel.IfBlock) stmt, genCtx);
         } else if (stmt instanceof LALScriptModel.DefStatement) {
-            LALBlockCodegen.generateDefStatement(
+            LALDefCodegen.generateDefStatement(
                 sb, (LALScriptModel.DefStatement) stmt, genCtx);
         }
     }
@@ -598,7 +604,7 @@ public final class LALClassGenerator {
                                           final LALScriptModel.IfBlock ifBlock,
                                           final GenCtx genCtx) {
         sb.append("  if (");
-        LALBlockCodegen.generateCondition(sb, ifBlock.getCondition(), genCtx);
+        LALValueCodegen.generateCondition(sb, ifBlock.getCondition(), genCtx);
         sb.append(") {\n");
         for (final LALScriptModel.FilterStatement s : ifBlock.getThenBranch()) {
             generateFilterStatement(sb, s, genCtx);
@@ -624,8 +630,9 @@ public final class LALClassGenerator {
         final Class<?> resolvedOutput = this.outputType != null
             ? this.outputType
             : org.apache.skywalking.oap.server.core.source.LogBuilder.class;
+        final ParserType pt = detectParserType(model.getStatements());
         final GenCtx genCtx = new GenCtx(
-            detectParserType(model.getStatements()), this.inputType, resolvedOutput);
+            pt, pt == ParserType.NONE ? this.inputType : null, resolvedOutput);
         final String execute = generateExecuteMethod(model, genCtx);
         if (genCtx.privateMethods.isEmpty()) {
             return execute;
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALCodegenHelper.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALCodegenHelper.java
index bb890a65fe..a463d13b4e 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALCodegenHelper.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALCodegenHelper.java
@@ -22,6 +22,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import org.apache.skywalking.apm.network.logging.v3.LogData;
 
 /**
  * Static utility constants and methods extracted from {@link LALClassGenerator}
@@ -29,27 +30,40 @@ import java.util.Set;
  */
 final class LALCodegenHelper {
 
+    /** Metadata fields — available on {@code LogMetadata} (always present). */
+    static final Map<String, String> METADATA_GETTERS = new HashMap<>();
+    /** TraceContext sub-fields on {@code LogMetadata.TraceContext}. */
+    static final Map<String, String> METADATA_TRACE_GETTERS = new HashMap<>();
+    /** LogData-only fields — require the input to be {@code LogData}. */
     static final Map<String, String> LOG_GETTERS = new HashMap<>();
-    static final Map<String, String> TRACE_CONTEXT_GETTERS = new HashMap<>();
+    /**
+     * Getter-name overrides for reflection-based access.
+     * Applied by {@link LALBlockCodegen#generateExtraLogAccess} when a getter
+     * is not found on the current type — tries the alias before failing.
+     */
+    static final Map<String, String> METADATA_GETTER_ALIASES = Map.of();
     static final Set<String> LONG_FIELDS = new HashSet<>();
     static final Set<String> INT_FIELDS = new HashSet<>();
+    static final Class<?> LOGDATA_BUILDER_CLASS = LogData.Builder.class;
 
     static {
-        LOG_GETTERS.put("service", "getService");
-        LOG_GETTERS.put("serviceInstance", "getServiceInstance");
-        LOG_GETTERS.put("endpoint", "getEndpoint");
-        LOG_GETTERS.put("timestamp", "getTimestamp");
+        METADATA_GETTERS.put("service", "getService");
+        METADATA_GETTERS.put("serviceInstance", "getServiceInstance");
+        METADATA_GETTERS.put("endpoint", "getEndpoint");
+        METADATA_GETTERS.put("layer", "getLayer");
+        METADATA_GETTERS.put("timestamp", "getTimestamp");
+        METADATA_GETTERS.put("traceContext", "getTraceContext");
+
+        METADATA_TRACE_GETTERS.put("traceId", "getTraceId");
+        METADATA_TRACE_GETTERS.put("traceSegmentId", "getTraceSegmentId");
+        METADATA_TRACE_GETTERS.put("spanId", "getSpanId");
+
         LOG_GETTERS.put("body", "getBody");
-        LOG_GETTERS.put("traceContext", "getTraceContext");
         LOG_GETTERS.put("tags", "getTags");
-        LOG_GETTERS.put("layer", "getLayer");
-
-        TRACE_CONTEXT_GETTERS.put("traceId", "getTraceId");
-        TRACE_CONTEXT_GETTERS.put("traceSegmentId", "getTraceSegmentId");
-        TRACE_CONTEXT_GETTERS.put("spanId", "getSpanId");
 
         LONG_FIELDS.add("timestamp");
         INT_FIELDS.add("spanId");
+
     }
 
     private LALCodegenHelper() {
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALDefCodegen.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALDefCodegen.java
new file mode 100644
index 0000000000..53afc2c584
--- /dev/null
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALDefCodegen.java
@@ -0,0 +1,360 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.log.analyzer.v2.compiler;
+
+import java.util.List;
+
+/**
+ * Code generation for LAL {@code def} local variable declarations and
+ * typed method-chain resolution on def variables.
+ *
+ * <p>The {@code def} keyword declares a local variable in the extractor
+ * block. The type is inferred from the initializer expression, and
+ * subsequent method chains are resolved via compile-time reflection.
+ *
+ * <p>LAL example:
+ * <pre>{@code
+ * extractor {
+ *   // def with built-in function — type inferred as JsonObject
+ *   def jwt = toJson(parsed?.commonProperties?.metadata?
+ *       .filterMetadataMap?.get("envoy.filters.http.jwt_authn"))
+ *
+ *   // def chaining on another def variable — type inferred as JsonObject
+ *   def payload = jwt?.getAsJsonObject("payload")
+ *
+ *   // Using def variable in tag assignment
+ *   tag 'email': payload?.get("email")?.getAsString()
+ *
+ *   // def with explicit type cast
+ *   def code = parsed?.response?.responseCode?.value as Integer
+ * }
+ * }</pre>
+ *
+ * <p>Generated code declares Java locals with {@code _def_} prefix
+ * (e.g. {@code _def_jwt}, {@code _def_payload}) and emits assignments
+ * at the point where {@code def} appears in the DSL.
+ */
+final class LALDefCodegen {
+
+    private LALDefCodegen() {
+        // utility class
+    }
+
+    // ==================== Def statement codegen ====================
+
+    /**
+     * Generates a {@code def} variable declaration and assignment.
+     *
+     * <p>LAL: {@code def jwt = toJson(parsed?.metadata?.filterMetadataMap?.get("key"))}
+     *
+     * <p>Generated (declaration at method top):
+     * <pre>{@code
+     * com.google.gson.JsonObject _def_jwt;
+     * }</pre>
+     * <p>Generated (assignment at def site):
+     * <pre>{@code
+     * _def_jwt = h.toJsonObject(h.mapVal("metadata", "filterMetadataMap", "key"));
+     * }</pre>
+     *
+     * <p>Type inference: built-in functions ({@code toJson} &rarr;
+     * {@code JsonObject}, {@code toJsonArray} &rarr; {@code JsonArray}) have
+     * known return types. General expressions infer the type from
+     * {@code genCtx.lastResolvedType} after codegen. An explicit
+     * {@code as Type} cast overrides the inferred type.
+     */
+    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 = "_def_" + varName;
+        final boolean alreadyDeclared = genCtx.localVars.containsKey(varName);
+
+        // Determine type and generate initializer expression
+        Class<?> resolvedType;
+        final StringBuilder initExpr = new StringBuilder();
+
+        if (init.getFunctionCallName() != null
+                && LALBlockCodegen.BUILTIN_FUNCTIONS.containsKey(init.getFunctionCallName())) {
+            // Built-in function: toJson(...), toJsonArray(...)
+            final String funcName = init.getFunctionCallName();
+            final int argCount = init.getFunctionCallArgs().size();
+            if (argCount != 1) {
+                throw new IllegalArgumentException(
+                    funcName + "() requires exactly 1 argument, got " + argCount);
+            }
+            final Object[] info = LALBlockCodegen.BUILTIN_FUNCTIONS.get(funcName);
+            final String helperMethod = (String) info[0];
+            resolvedType = (Class<?>) info[1];
+
+            initExpr.append(helperMethod).append("(");
+            LALValueCodegen.generateValueAccess(initExpr,
+                init.getFunctionCallArgs().get(0).getValue(), genCtx);
+            initExpr.append(")");
+        } else {
+            // General value access — type inferred from lastResolvedType
+            LALValueCodegen.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) — skip if already declared
+        if (!alreadyDeclared) {
+            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 =
+                    LALValueCodegen.generateMethodArgs(ms.getArguments(), genCtx);
+
+                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;
+    }
+}
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALValueCodegen.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALValueCodegen.java
new file mode 100644
index 0000000000..63588f34b1
--- /dev/null
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALValueCodegen.java
@@ -0,0 +1,903 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.log.analyzer.v2.compiler;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.skywalking.oap.server.core.source.LogMetadata;
+
+/**
+ * Code generation for LAL expression evaluation: value access, conditions,
+ * type casts, and all data-source-specific access patterns.
+ *
+ * <p>This class handles the expression-level codegen that
+ * {@link LALBlockCodegen} delegates to when it encounters value expressions
+ * inside block statements. The central dispatch method is
+ * {@link #generateValueAccess}, which routes each expression to the
+ * appropriate codegen path:
+ *
+ * <pre>{@code
+ * Expression type          LAL example                        Generated code
+ * ─────────────────────── ─────────────────────────────────── ────────────────────────────
+ * tag() function           tag("LOG_KIND")                    h.tagValue("LOG_KIND")
+ * String literal           "SLOW_SQL"                         "SLOW_SQL"
+ * Number literal           500                                Integer.valueOf(500)
+ * ProcessRegistry          ProcessRegistry.generateVirtual..  ProcessRegistry.generate...
+ * log.* field              log.traceContext.traceId            h.ctx().metadata().get...
+ * parsed.* (json/yaml)     parsed.service                     h.mapVal("service")
+ * parsed.* (text)          parsed.level                       h.group("level")
+ * parsed.* (typed proto)   parsed?.response?.responseCode     ((Type)_p).getResponse()...
+ * def variable             myVar?.getAsString()               _def_myVar?.getAsString()
+ * Parenthesized            (expr as String).trim()            h.toStr(...).trim()
+ * String concat            "${log.service}:${parsed.code}"    "" + ... + ":" + ...
+ * }</pre>
+ *
+ * <p>Condition codegen ({@link #generateCondition}) handles {@code if}
+ * conditions: comparisons ({@code ==}, {@code !=}, {@code <}, {@code >}),
+ * logical operators ({@code &&}, {@code ||}), negation ({@code !}), and
+ * expression-as-boolean ({@code if (parsed?.flags?.toString())}).
+ */
+final class LALValueCodegen {
+
+    static final String LOGDATA_BUILDER_CAST =
+        "((org.apache.skywalking.apm.network.logging.v3.LogData.Builder) h.ctx().input())";
+    static final String PROCESS_REGISTRY =
+        "org.apache.skywalking.oap.meter.analyzer.v2.dsl.registry.ProcessRegistry";
+
+    private LALValueCodegen() {
+        // utility class
+    }
+
+    // ==================== Conditions ====================
+
+    /**
+     * Generates code for an {@code if} condition expression.
+     *
+     * <p>LAL examples and their generated code:
+     * <pre>{@code
+     * // Comparison (==, !=):
+     * if (tag("LOG_KIND") == "SLOW_SQL")
+     * → java.util.Objects.equals(h.tagValue("LOG_KIND"), "SLOW_SQL")
+     *
+     * // Numeric comparison (<, >, <=, >=):
+     * if (parsed?.response?.responseCode?.value as Integer < 400)
+     * → ((Type)_p).getResponse().getResponseCode().getValue() < 400L
+     *
+     * // Logical operators:
+     * if (condA && condB)  →  (condA && condB)
+     * if (!cond)           →  !(cond)
+     *
+     * // Expression-as-boolean (truthy check):
+     * if (parsed?.commonProperties?.responseFlags?.toString())
+     * → h.isNotEmpty(h.toString(...))
+     * }</pre>
+     */
+    static void generateCondition(final StringBuilder sb,
+                                   final LALScriptModel.Condition cond,
+                                   final LALClassGenerator.GenCtx genCtx) {
+        if (cond instanceof LALScriptModel.ComparisonCondition) {
+            final LALScriptModel.ComparisonCondition cc =
+                (LALScriptModel.ComparisonCondition) cond;
+            switch (cc.getOp()) {
+                case EQ:
+                    sb.append("java.util.Objects.equals(");
+                    generateValueAccessObj(sb, cc.getLeft(), cc.getLeftCast(), genCtx);
+                    sb.append(", ");
+                    generateConditionValue(sb, cc.getRight(), genCtx);
+                    sb.append(")");
+                    break;
+                case NEQ:
+                    sb.append("!java.util.Objects.equals(");
+                    generateValueAccessObj(sb, cc.getLeft(), cc.getLeftCast(), genCtx);
+                    sb.append(", ");
+                    generateConditionValue(sb, cc.getRight(), genCtx);
+                    sb.append(")");
+                    break;
+                case GT:
+                    generateNumericComparison(sb, cc, " > ", genCtx);
+                    break;
+                case LT:
+                    generateNumericComparison(sb, cc, " < ", genCtx);
+                    break;
+                case GTE:
+                    generateNumericComparison(sb, cc, " >= ", genCtx);
+                    break;
+                case LTE:
+                    generateNumericComparison(sb, cc, " <= ", genCtx);
+                    break;
+                default:
+                    break;
+            }
+        } else if (cond instanceof LALScriptModel.LogicalCondition) {
+            final LALScriptModel.LogicalCondition lc =
+                (LALScriptModel.LogicalCondition) cond;
+            sb.append("(");
+            generateCondition(sb, lc.getLeft(), genCtx);
+            sb.append(lc.getOp() == LALScriptModel.LogicalOp.AND
+                ? " && " : " || ");
+            generateCondition(sb, lc.getRight(), genCtx);
+            sb.append(")");
+        } else if (cond instanceof LALScriptModel.NotCondition) {
+            sb.append("!(");
+            generateCondition(sb,
+                ((LALScriptModel.NotCondition) cond).getInner(), genCtx);
+            sb.append(")");
+        } else if (cond instanceof LALScriptModel.ExprCondition) {
+            final String ct = ((LALScriptModel.ExprCondition) cond).getCastType();
+            final String method = "Boolean".equals(ct) || "boolean".equals(ct)
+                ? ".isTrue(" : ".isNotEmpty(";
+            sb.append("h").append(method);
+            generateValueAccessObj(sb,
+                ((LALScriptModel.ExprCondition) cond).getExpr(),
+                ct, genCtx);
+            sb.append(")");
+        }
+    }
+
+    static void generateNumericComparison(
+            final StringBuilder sb,
+            final LALScriptModel.ComparisonCondition cc,
+            final String op,
+            final LALClassGenerator.GenCtx genCtx) {
+        // Generate left side into buffer to inspect resolved type
+        final StringBuilder leftBuf = new StringBuilder();
+        generateValueAccessObj(leftBuf, cc.getLeft(), null, genCtx);
+
+        final boolean primitiveNumeric = genCtx.lastResolvedType != null
+            && (genCtx.lastResolvedType == int.class
+                || genCtx.lastResolvedType == long.class);
+
+        if (primitiveNumeric && genCtx.lastRawChain != null) {
+            // Direct primitive comparison — no boxing, no h.toLong()
+            if (genCtx.lastNullChecks != null) {
+                sb.append("(").append(genCtx.lastNullChecks).append(" ? false : ")
+                  .append(genCtx.lastRawChain).append(op);
+                generateConditionValueNumeric(sb, cc.getRight(), genCtx);
+                sb.append(")");
+            } else {
+                sb.append(genCtx.lastRawChain).append(op);
+                generateConditionValueNumeric(sb, cc.getRight(), genCtx);
+            }
+        } else {
+            // Fallback: h.toLong() conversion
+            sb.append("h.toLong(").append(leftBuf).append(")").append(op);
+            generateConditionValueNumeric(sb, cc.getRight(), genCtx);
+        }
+    }
+
+    static void generateConditionValue(final StringBuilder sb,
+                                        final LALScriptModel.ConditionValue cv,
+                                        final LALClassGenerator.GenCtx genCtx) {
+        if (cv instanceof LALScriptModel.StringConditionValue) {
+            sb.append('"')
+              .append(LALCodegenHelper.escapeJava(
+                  ((LALScriptModel.StringConditionValue) cv).getValue()))
+              .append('"');
+        } else if (cv instanceof LALScriptModel.NumberConditionValue) {
+            final double val =
+                ((LALScriptModel.NumberConditionValue) cv).getValue();
+            sb.append("Long.valueOf(").append((long) val).append("L)");
+        } else if (cv instanceof LALScriptModel.BoolConditionValue) {
+            sb.append("Boolean.valueOf(")
+              .append(((LALScriptModel.BoolConditionValue) cv).isValue())
+              .append(")");
+        } else if (cv instanceof LALScriptModel.NullConditionValue) {
+            sb.append("null");
+        } else if (cv instanceof LALScriptModel.ValueAccessConditionValue) {
+            generateValueAccessObj(sb,
+                ((LALScriptModel.ValueAccessConditionValue) cv).getValue(),
+                null, genCtx);
+        }
+    }
+
+    static void generateConditionValueNumeric(
+            final StringBuilder sb,
+            final LALScriptModel.ConditionValue cv,
+            final LALClassGenerator.GenCtx genCtx) {
+        if (cv instanceof LALScriptModel.NumberConditionValue) {
+            sb.append((long) ((LALScriptModel.NumberConditionValue) cv)
+                .getValue()).append("L");
+        } else if (cv instanceof LALScriptModel.ValueAccessConditionValue) {
+            sb.append("h.toLong(");
+            generateValueAccessObj(sb,
+                ((LALScriptModel.ValueAccessConditionValue) cv).getValue(),
+                null, genCtx);
+            sb.append(")");
+        } else {
+            sb.append("0L");
+        }
+    }
+
+    // ==================== Value access ====================
+
+    /**
+     * Generates a value access with an explicit type cast.
+     *
+     * <p>LAL: {@code parsed.latency as Long}
+     * <br>Generated: {@code h.toLong(h.mapVal("latency"))}
+     *
+     * <p>Supported cast types: String, Long, Integer, Boolean.
+     */
+    static void generateCastedValueAccess(final StringBuilder sb,
+                                           final LALScriptModel.ValueAccess value,
+                                           final String castType,
+                                           final LALClassGenerator.GenCtx genCtx) {
+        if ("String".equals(castType)) {
+            sb.append("h.toStr(");
+            generateValueAccess(sb, value, genCtx);
+            sb.append(")");
+        } else if ("Long".equals(castType)) {
+            sb.append("h.toLong(");
+            generateValueAccess(sb, value, genCtx);
+            sb.append(")");
+        } else if ("Integer".equals(castType)) {
+            sb.append("h.toInt(");
+            generateValueAccess(sb, value, genCtx);
+            sb.append(")");
+        } else if ("Boolean".equals(castType)) {
+            sb.append("h.toBool(");
+            generateValueAccess(sb, value, genCtx);
+            sb.append(")");
+        } else {
+            generateValueAccess(sb, value, genCtx);
+        }
+    }
+
+    /**
+     * Generates a value access that must produce a String result (used by
+     * tag assignments). Wraps the inner expression with {@code h.toStr()}.
+     */
+    static void generateStringValueAccess(final StringBuilder sb,
+                                            final LALScriptModel.ValueAccess value,
+                                            final String castType,
+                                            final LALClassGenerator.GenCtx genCtx) {
+        if (castType == null || "String".equals(castType)) {
+            sb.append("h.toStr(");
+            generateValueAccess(sb, value, genCtx);
+            sb.append(")");
+        } else if ("Long".equals(castType)) {
+            sb.append("String.valueOf(h.toLong(");
+            generateValueAccess(sb, value, genCtx);
+            sb.append("))");
+        } else if ("Integer".equals(castType)) {
+            sb.append("String.valueOf(h.toInt(");
+            generateValueAccess(sb, value, genCtx);
+            sb.append("))");
+        } else if ("Boolean".equals(castType)) {
+            sb.append("String.valueOf(h.toBool(");
+            generateValueAccess(sb, value, genCtx);
+            sb.append("))");
+        } else {
+            sb.append("h.toStr(");
+            generateValueAccess(sb, value, genCtx);
+            sb.append(")");
+        }
+    }
+
+    /**
+     * Generates a value access that produces an Object (boxed) result.
+     * Only applies String cast; other casts pass through as-is.
+     */
+    static void generateValueAccessObj(final StringBuilder sb,
+                                        final LALScriptModel.ValueAccess value,
+                                        final String castType,
+                                        final LALClassGenerator.GenCtx genCtx) {
+        if ("String".equals(castType)) {
+            sb.append("h.toStr(");
+            generateValueAccess(sb, value, genCtx);
+            sb.append(")");
+        } else {
+            generateValueAccess(sb, value, genCtx);
+        }
+    }
+
+    /**
+     * Central dispatch for all LAL value-access expressions. Routes each
+     * expression to the appropriate codegen path based on its AST type.
+     *
+     * <p>Dispatch order:
+     * <ol>
+     *   <li>String concatenation ({@code "..." + expr + "..."})</li>
+     *   <li>Parenthesized expression ({@code (expr as Type).chain})</li>
+     *   <li>Standalone {@code tag("KEY")} function call</li>
+     *   <li>String/number literals</li>
+     *   <li>{@code ProcessRegistry.method(...)} static calls</li>
+     *   <li>{@code log.X.Y} direct proto getter chains</li>
+     *   <li>{@code parsed.X.Y} with compile-time type analysis</li>
+     *   <li>{@code def} variable reference with method chain</li>
+     *   <li>Fallback: compile-time error</li>
+     * </ol>
+     *
+     * <p>Note: standalone {@code tag("KEY")} and {@code parsed.tag("KEY")}
+     * are different AST structures. {@code tag("KEY")} is a top-level
+     * function call handled here; {@code parsed.tag("KEY")} is a
+     * {@code parsed}-prefixed chain where {@code tag("KEY")} appears as a
+     * MethodSegment, handled by {@link #generateParsedAccess}.
+     */
+    static void generateValueAccess(final StringBuilder sb,
+                                     final LALScriptModel.ValueAccess value,
+                                     final LALClassGenerator.GenCtx genCtx) {
+        genCtx.clearExtraLogResult();
+
+        // Handle string concatenation (term1 + term2 + ...)
+        if (!value.getConcatParts().isEmpty()) {
+            sb.append("(\"\" + ");
+            for (int i = 0; i < value.getConcatParts().size(); i++) {
+                if (i > 0) {
+                    sb.append(" + ");
+                }
+                generateValueAccess(sb, value.getConcatParts().get(i), genCtx);
+            }
+            sb.append(")");
+            return;
+        }
+
+        // Handle parenthesized expression: (innerExpr as Type).chain...
+        if (value.getParenInner() != null) {
+            generateParenAccess(sb, value, genCtx);
+            return;
+        }
+
+        // tag("KEY") — reads LogData tags via LalRuntimeHelper.tagValue().
+        // Only valid when input is LogData.Builder (i.e. no inputType, or inputType
+        // is LogData.Builder).  For typed inputs, tag() is not supported — use
+        // parsed.* to access fields on the typed input.
+        if ("tag".equals(value.getFunctionCallName())) {
+            if (value.getFunctionCallArgs().size() != 1
+                    || !value.getFunctionCallArgs().get(0).getValue().isStringLiteral()) {
+                throw new IllegalArgumentException(
+                    "tag() requires exactly one string literal argument, "
+                        + "e.g. tag(\"KEY\")");
+            }
+            if (genCtx.inputType != null
+                    && !LALCodegenHelper.LOGDATA_BUILDER_CLASS
+                        .isAssignableFrom(genCtx.inputType)) {
+                throw new IllegalArgumentException(
+                    "tag() reads LogData tags but the input type is "
+                        + genCtx.inputType.getName()
+                        + ". Use a json{}/yaml{}/text{} parser, or access "
+                        + "typed fields via parsed.* instead.");
+            }
+            sb.append("h.tagValue(\"");
+            final String key = value.getFunctionCallArgs().get(0)
+                .getValue().getSegments().get(0);
+            sb.append(LALCodegenHelper.escapeJava(key)).append("\")");
+            return;
+        }
+
+        // Handle string/number literals
+        if (value.isStringLiteral() && value.getChain().isEmpty()) {
+            sb.append("\"").append(LALCodegenHelper.escapeJava(value.getSegments().get(0)))
+              .append("\"");
+            return;
+        }
+        if (value.isNumberLiteral() && value.getChain().isEmpty()) {
+            final String num = value.getSegments().get(0);
+            if (num.contains(".")) {
+                sb.append("Double.valueOf(").append(num).append(")");
+            } else {
+                sb.append("Integer.valueOf(").append(num).append(")");
+            }
+            return;
+        }
+
+        // Handle ProcessRegistry static calls
+        if (value.isProcessRegistryRef()) {
+            generateProcessRegistryCall(sb, value, genCtx);
+            return;
+        }
+
+        final List<LALScriptModel.ValueAccessSegment> chain = value.getChain();
+
+        // Handle log.X.Y direct proto getter chains
+        if (value.isLogRef()) {
+            generateLogAccess(sb, chain);
+            return;
+        }
+
+        // Handle parsed.X.Y with compile-time type analysis
+        if (value.isParsedRef()) {
+            generateParsedAccess(sb, chain, genCtx);
+            return;
+        }
+
+        // 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) {
+                LALDefCodegen.generateDefVarChain(sb, localVar, chain, genCtx);
+                return;
+            }
+        }
+
+        // No matching codegen path — fail at compile time instead of silently
+        // generating null.
+        if (chain.isEmpty()) {
+            final String desc = value.getFunctionCallName() != null
+                ? "function call '" + value.getFunctionCallName() + "(...)'"
+                : "expression '" + value.getSegments() + "'";
+            throw new IllegalArgumentException(
+                "Cannot resolve " + desc + " — no matching codegen path. "
+                    + "Parser type: " + genCtx.parserType
+                    + (genCtx.inputType != null
+                        ? ", inputType: " + genCtx.inputType.getName() : ""));
+        }
+        // Treat as parsed ref
+        generateParsedAccess(sb, chain, genCtx);
+    }
+
+    // ==================== Parenthesized expression ====================
+
+    /**
+     * Generates code for a parenthesized expression with optional type cast
+     * and method chain.
+     *
+     * <p>LAL: {@code (parsed.component as String).trim()}
+     * <br>Generated: {@code h.toStr(h.mapVal("component")).trim()}
+     */
+    static void generateParenAccess(final StringBuilder sb,
+                                     final LALScriptModel.ValueAccess value,
+                                     final LALClassGenerator.GenCtx genCtx) {
+        // Generate the inner expression with cast
+        final String castType = value.getParenCast();
+        final StringBuilder inner = new StringBuilder();
+        if (castType != null) {
+            generateCastedValueAccess(inner, value.getParenInner(), castType, genCtx);
+        } else {
+            generateValueAccess(inner, value.getParenInner(), genCtx);
+        }
+
+        // Apply chain segments (methods, fields, index access)
+        String current = inner.toString();
+        for (final LALScriptModel.ValueAccessSegment seg : value.getChain()) {
+            if (seg instanceof LALScriptModel.MethodSegment) {
+                current = appendMethodSegment(current,
+                    (LALScriptModel.MethodSegment) seg, genCtx);
+            } else if (seg instanceof LALScriptModel.IndexSegment) {
+                current = current + "["
+                    + ((LALScriptModel.IndexSegment) seg).getIndex() + "]";
+            } else if (seg instanceof LALScriptModel.FieldSegment) {
+                final LALScriptModel.FieldSegment fs =
+                    (LALScriptModel.FieldSegment) seg;
+                if (fs.isSafeNav()) {
+                    current = "(" + current + " == null ? null : "
+                        + current + "." + fs.getName() + ")";
+                } else {
+                    current = current + "." + fs.getName();
+                }
+            }
+        }
+        sb.append(current);
+    }
+
+    // ==================== Log access (direct proto getters) ====================
+
+    /**
+     * Generate code for {@code log.xxx} field access in LAL scripts.
+     * <ul>
+     *   <li>Metadata fields (service, endpoint, layer, timestamp, traceContext)
+     *       &rarr; {@code h.ctx().metadata().getXxx()}</li>
+     *   <li>LogData-only fields (body, tags)
+     *       &rarr; {@code ((LogData.Builder) h.ctx().input()).getXxx()}</li>
+     * </ul>
+     */
+    static void generateLogAccess(final StringBuilder sb,
+                                   final List<LALScriptModel.ValueAccessSegment> chain) {
+        if (chain.isEmpty()) {
+            sb.append("h.ctx().input()");
+            return;
+        }
+
+        String current;
+        boolean needsBoxing = false;
+        String boxType = null;
+
+        // Determine root based on first field
+        final LALScriptModel.ValueAccessSegment first = chain.get(0);
+        if (!(first instanceof LALScriptModel.FieldSegment)) {
+            current = LOGDATA_BUILDER_CAST;
+        } else {
+            final String firstName = ((LALScriptModel.FieldSegment) first).getName();
+            if (LALCodegenHelper.METADATA_GETTERS.containsKey(firstName)) {
+                current = "h.ctx().metadata()";
+            } else if (LALCodegenHelper.LOG_GETTERS.containsKey(firstName)) {
+                current = LOGDATA_BUILDER_CAST;
+            } else {
+                throw new IllegalArgumentException(
+                    "Unknown log field: log." + firstName
+                        + ". Supported metadata fields: "
+                        + LALCodegenHelper.METADATA_GETTERS.keySet()
+                        + ", LogData fields: "
+                        + LALCodegenHelper.LOG_GETTERS.keySet());
+            }
+        }
+
+        for (int i = 0; i < chain.size(); i++) {
+            final LALScriptModel.ValueAccessSegment seg = chain.get(i);
+            if (seg instanceof LALScriptModel.FieldSegment) {
+                final String name = ((LALScriptModel.FieldSegment) seg).getName();
+                if (i == 0 && LALCodegenHelper.METADATA_GETTERS.containsKey(name)) {
+                    if ("traceContext".equals(name)) {
+                        current = current + ".getTraceContext()";
+                    } else {
+                        current = current + "."
+                            + LALCodegenHelper.METADATA_GETTERS.get(name) + "()";
+                        if (LALCodegenHelper.LONG_FIELDS.contains(name)) {
+                            needsBoxing = true;
+                            boxType = "Long";
+                        }
+                    }
+                } else if (i == 0 && LALCodegenHelper.LOG_GETTERS.containsKey(name)) {
+                    current = current + "."
+                        + LALCodegenHelper.LOG_GETTERS.get(name) + "()";
+                } else if (i == 1 && current.endsWith(".getTraceContext()")
+                        && LALCodegenHelper.METADATA_TRACE_GETTERS.containsKey(name)) {
+                    current = current + "."
+                        + LALCodegenHelper.METADATA_TRACE_GETTERS.get(name) + "()";
+                    if (LALCodegenHelper.INT_FIELDS.contains(name)) {
+                        needsBoxing = true;
+                        boxType = "Integer";
+                    }
+                } else {
+                    throw new IllegalArgumentException(
+                        "Unknown log field: log." + name
+                            + ". Supported metadata fields: "
+                            + LALCodegenHelper.METADATA_GETTERS.keySet()
+                            + ", traceContext."
+                            + LALCodegenHelper.METADATA_TRACE_GETTERS.keySet()
+                            + ", LogData fields: "
+                            + LALCodegenHelper.LOG_GETTERS.keySet());
+                }
+            } else if (seg instanceof LALScriptModel.MethodSegment) {
+                current = appendMethodSegment(current,
+                    (LALScriptModel.MethodSegment) seg, null);
+            }
+        }
+
+        if (needsBoxing) {
+            sb.append(boxType).append(".valueOf(").append(current).append(")");
+        } else {
+            sb.append(current);
+        }
+    }
+
+    // ==================== Parsed access (compile-time typed) ====================
+
+    /**
+     * Generates code for {@code parsed.X.Y} field access, with different
+     * strategies depending on the parser type:
+     *
+     * <pre>{@code
+     * Parser     LAL                             Generated
+     * ────────── ──────────────────────────────── ──────────────────────────
+     * json/yaml  parsed.service                   h.mapVal("service")
+     * json/yaml  parsed.client_process.address    h.mapVal("client_process", "address")
+     * text       parsed.level                     h.group("level")
+     * none+type  parsed?.response?.responseCode   ((Type)_p).getResponse()...
+     * none       parsed.service                   h.ctx().metadata().getService()
+     * }</pre>
+     *
+     * <p>For typed proto access ({@code NONE} parser with {@code inputType}),
+     * uses compile-time reflection to generate direct getter chains with
+     * null-safe navigation and local variable caching.
+     */
+    static void generateParsedAccess(
+            final StringBuilder sb,
+            final List<LALScriptModel.ValueAccessSegment> chain,
+            final LALClassGenerator.GenCtx genCtx) {
+        if (chain.isEmpty()) {
+            sb.append("h.ctx().parsed()");
+            return;
+        }
+
+        // Collect leading field segments (stop at method/index)
+        final List<LALScriptModel.FieldSegment> fieldSegments = new ArrayList<>();
+        int methodStart = -1;
+        for (int i = 0; i < chain.size(); i++) {
+            final LALScriptModel.ValueAccessSegment seg = chain.get(i);
+            if (seg instanceof LALScriptModel.FieldSegment) {
+                fieldSegments.add((LALScriptModel.FieldSegment) seg);
+            } else {
+                methodStart = i;
+                break;
+            }
+        }
+
+        final List<String> fieldKeys = new ArrayList<>();
+        for (final LALScriptModel.FieldSegment fs : fieldSegments) {
+            fieldKeys.add(fs.getName());
+        }
+
+        String current;
+        switch (genCtx.parserType) {
+            case JSON:
+            case YAML:
+                current = LALCodegenHelper.generateMapValCall(fieldKeys);
+                break;
+            case TEXT:
+                if (!fieldKeys.isEmpty()) {
+                    current = "h.group(\""
+                        + LALCodegenHelper.escapeJava(fieldKeys.get(0)) + "\")";
+                } else {
+                    current = "h.ctx().parsed()";
+                }
+                break;
+            case NONE:
+                if (genCtx.inputType != null) {
+                    current = generateExtraLogAccess(fieldSegments, genCtx.inputType,
+                        "_p", true, genCtx);
+                } else {
+                    // No parser and no inputType — fall back to LogMetadata
+                    current = generateExtraLogAccess(fieldSegments, LogMetadata.class,
+                        "h.ctx().metadata()", false, genCtx);
+                }
+                break;
+            default:
+                current = "null";
+                break;
+        }
+
+        // Apply remaining method/index segments
+        if (methodStart >= 0) {
+            for (int i = methodStart; i < chain.size(); i++) {
+                final LALScriptModel.ValueAccessSegment seg = chain.get(i);
+                if (seg instanceof LALScriptModel.MethodSegment) {
+                    current = appendMethodSegment(current,
+                        (LALScriptModel.MethodSegment) seg, genCtx);
+                } else if (seg instanceof LALScriptModel.IndexSegment) {
+                    current = current + "["
+                        + ((LALScriptModel.IndexSegment) seg).getIndex() + "]";
+                } else if (seg instanceof LALScriptModel.FieldSegment) {
+                    current = current + "."
+                        + ((LALScriptModel.FieldSegment) seg).getName();
+                }
+            }
+        }
+
+        sb.append(current);
+    }
+
+    /**
+     * Generates a reflection-based getter chain on a typed root object.
+     * Used for both typed proto access ({@code parsed?.response?.responseCode})
+     * and LogMetadata fallback ({@code parsed.service} without a parser).
+     *
+     * <p>Each getter call is validated at compile time via reflection. Safe
+     * navigation ({@code ?.}) generates null checks. Intermediate results
+     * are cached in local variables ({@code _t0}, {@code _t1}, ...) to avoid
+     * redundant getter calls when the same chain prefix appears multiple times.
+     */
+    static String generateExtraLogAccess(
+            final List<LALScriptModel.FieldSegment> fieldSegments,
+            final Class<?> rootType,
+            final String rootExpr,
+            final boolean rootCanBeNull,
+            final LALClassGenerator.GenCtx genCtx) {
+        genCtx.usedProtoAccess = true;
+
+        if (fieldSegments.isEmpty()) {
+            return rootExpr;
+        }
+
+        final String typeName = rootType.getName();
+        final StringBuilder chainKey = new StringBuilder();
+        String prevVar = rootExpr;
+        Class<?> currentType = rootType;
+        boolean prevCanBeNull = rootCanBeNull;
+
+        for (int i = 0; i < fieldSegments.size(); i++) {
+            final LALScriptModel.FieldSegment seg = fieldSegments.get(i);
+            final String field = seg.getName();
+            String getterName = "get" + Character.toUpperCase(field.charAt(0))
+                + field.substring(1);
+
+            // Apply getter aliases (e.g., traceSegmentId → segmentId on LogMetadata)
+            final String alias = LALCodegenHelper.METADATA_GETTER_ALIASES.get(getterName);
+            if (alias != null) {
+                try {
+                    currentType.getMethod(getterName);
+                } catch (NoSuchMethodException ignored) {
+                    getterName = alias;
+                }
+            }
+
+            final java.lang.reflect.Method getter;
+            try {
+                getter = currentType.getMethod(getterName);
+            } catch (NoSuchMethodException e) {
+                throw new IllegalArgumentException(
+                    "Cannot resolve getter " + currentType.getSimpleName()
+                        + "." + getterName + "() for type "
+                        + typeName + ". Check the field path in the LAL rule.");
+            }
+            final Class<?> returnType = getter.getReturnType();
+
+            if (chainKey.length() > 0) {
+                chainKey.append(".");
+            }
+            chainKey.append(field);
+            final String key = chainKey.toString();
+            final boolean isLast = i == fieldSegments.size() - 1;
+
+            // Primitive final segment: return inline expression, no variable
+            if (isLast && returnType.isPrimitive()) {
+                final String rawAccess = prevVar + "." + getterName + "()";
+                genCtx.lastResolvedType = returnType;
+                genCtx.lastRawChain = rawAccess;
+                final String boxName = LALCodegenHelper.boxTypeName(returnType);
+                if (seg.isSafeNav() && prevCanBeNull) {
+                    genCtx.lastNullChecks = prevVar + " == null";
+                    return "(" + prevVar + " == null ? null : "
+                        + boxName + ".valueOf(" + rawAccess + "))";
+                } else {
+                    genCtx.lastNullChecks = null;
+                    return boxName + ".valueOf(" + rawAccess + ")";
+                }
+            }
+
+            // Reuse existing variable (dedup)
+            final String existingVar = genCtx.protoVars.get(key);
+            if (existingVar != null) {
+                prevVar = existingVar;
+                currentType = returnType;
+                prevCanBeNull = true;
+                continue;
+            }
+
+            // Create new local variable declaration
+            final String newVar = "_t" + genCtx.protoVarCounter++;
+            final String returnTypeName = returnType.getName();
+            if (seg.isSafeNav() && prevCanBeNull) {
+                genCtx.protoVarDecls.append("  ").append(returnTypeName)
+                    .append(" ").append(newVar).append(" = ")
+                    .append(prevVar).append(" == null ? null : ")
+                    .append(prevVar).append(".").append(getterName).append("();\n");
+                prevCanBeNull = true;
+            } else {
+                genCtx.protoVarDecls.append("  ").append(returnTypeName)
+                    .append(" ").append(newVar).append(" = ")
+                    .append(prevVar).append(".").append(getterName).append("();\n");
+                prevCanBeNull = !returnType.isPrimitive();
+            }
+            genCtx.protoVars.put(key, newVar);
+            genCtx.protoLvtVars.add(new String[]{
+                newVar, "L" + returnTypeName.replace('.', '/') + ";"
+            });
+
+            prevVar = newVar;
+            currentType = returnType;
+        }
+
+        // Non-primitive final result — null checks are in declarations
+        genCtx.lastResolvedType = currentType;
+        genCtx.lastRawChain = prevVar;
+        genCtx.lastNullChecks = null;
+        return prevVar;
+    }
+
+    // ==================== ProcessRegistry ====================
+
+    /**
+     * Generates a {@code ProcessRegistry} static method call.
+     *
+     * <p>LAL: {@code ProcessRegistry.generateVirtualRemoteProcess(
+     *     parsed.service as String, parsed.serviceInstance as String,
+     *     parsed.client_process.address as String)}
+     * <br>Generated: {@code ProcessRegistry.generateVirtualRemoteProcess(
+     *     h.toStr(...), h.toStr(...), h.toStr(...))}
+     */
+    static void generateProcessRegistryCall(
+            final StringBuilder sb,
+            final LALScriptModel.ValueAccess value,
+            final LALClassGenerator.GenCtx genCtx) {
+        final List<LALScriptModel.ValueAccessSegment> chain = value.getChain();
+        if (chain.isEmpty()) {
+            sb.append("null");
+            return;
+        }
+        final LALScriptModel.ValueAccessSegment seg = chain.get(0);
+        if (seg instanceof LALScriptModel.MethodSegment) {
+            final LALScriptModel.MethodSegment ms =
+                (LALScriptModel.MethodSegment) seg;
+            sb.append(PROCESS_REGISTRY).append(".")
+              .append(ms.getName()).append("(");
+            final List<LALScriptModel.FunctionArg> args = ms.getArguments();
+            for (int i = 0; i < args.size(); i++) {
+                if (i > 0) {
+                    sb.append(", ");
+                }
+                generateCastedValueAccess(sb,
+                    args.get(i).getValue(), args.get(i).getCastType(), genCtx);
+            }
+            sb.append(")");
+        } else {
+            sb.append("null");
+        }
+    }
+
+    // ==================== Utility methods ====================
+
+    /**
+     * Appends a method call segment to the current expression chain.
+     * Handles safe-navigation ({@code ?.method()}) by wrapping in a null
+     * guard. Special-cases {@code ?.toString()} and {@code ?.trim()} to
+     * use helper methods for null-safe behavior.
+     */
+    static String appendMethodSegment(final String current,
+                                       final LALScriptModel.MethodSegment ms,
+                                       final LALClassGenerator.GenCtx genCtx) {
+        final String mn = ms.getName();
+        final String args = ms.getArguments().isEmpty()
+            ? "" : generateMethodArgs(ms.getArguments(), genCtx);
+        if (ms.isSafeNav()) {
+            // 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 + ")";
+            }
+            // General safe-nav: null guard with ternary
+            return "(" + current + " == null ? null : "
+                + current + "." + mn + "(" + args + "))";
+        } else {
+            return current + "." + mn + "(" + args + ")";
+        }
+    }
+
+    /**
+     * Generates the argument list for a method call. Handles string literals,
+     * number literals, boolean/null literals, and def variable references.
+     */
+    static String generateMethodArgs(
+            final List<LALScriptModel.FunctionArg> args,
+            final LALClassGenerator.GenCtx genCtx) {
+        final StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < args.size(); i++) {
+            if (i > 0) {
+                sb.append(", ");
+            }
+            final LALScriptModel.FunctionArg arg = args.get(i);
+            final LALScriptModel.ValueAccess va = arg.getValue();
+            if (va.isStringLiteral()) {
+                sb.append("\"").append(LALCodegenHelper.escapeJava(
+                    va.getSegments().get(0))).append("\"");
+            } else if (va.isNumberLiteral()) {
+                sb.append(va.getSegments().get(0));
+            } else if (!va.getSegments().isEmpty()) {
+                final String text = va.getSegments().get(0);
+                if ("true".equals(text) || "false".equals(text)
+                        || "null".equals(text)) {
+                    // Boolean or null literal
+                    sb.append(text);
+                } else if (genCtx != null
+                        && genCtx.localVars.containsKey(text)) {
+                    // Local def variable reference
+                    sb.append(genCtx.localVars.get(text).javaVarName);
+                } else {
+                    throw new IllegalArgumentException(
+                        "Unknown identifier used as method argument: '" + text + "'");
+                }
+            } else {
+                sb.append("null");
+            }
+        }
+        return sb.toString();
+    }
+}
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/rt/LalRuntimeHelper.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/rt/LalRuntimeHelper.java
index 56ad4b706a..4ffd24ae1d 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
@@ -31,6 +31,7 @@ import java.util.List;
 import java.util.Map;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.skywalking.apm.network.common.v3.KeyStringValuePair;
+import org.apache.skywalking.apm.network.logging.v3.LogData;
 import org.apache.skywalking.oap.log.analyzer.v2.dsl.ExecutionContext;
 import org.apache.skywalking.oap.server.library.util.StringUtil;
 
@@ -67,13 +68,17 @@ import org.apache.skywalking.oap.server.library.util.StringUtil;
  *   // Generated:  h.tagValue("LOG_KIND")
  * </pre>
  *
- * <p><b>4. Log proto data</b> — direct access to {@code LogData.Builder} fields.
+ * <p><b>4. Metadata</b> — direct access to {@code LogMetadata} fields.
  * Not accessed through this helper; the compiler generates direct getter chains
- * like {@code h.ctx().log().getService()}.
+ * like {@code h.ctx().metadata().getService()}.
  *
- * <p><b>5. ExtraLog proto data</b> — direct access to typed protobuf extraLog.
+ * <p><b>5. LogData body/tags</b> — direct access to {@code LogData.Builder} fields.
  * Not accessed through this helper; the compiler generates typed cast + getter
- * chains like {@code ((HTTPAccessLogEntry) h.ctx().extraLog()).getResponse()}.
+ * chains like {@code ((LogData.Builder) h.ctx().input()).getBody()}.
+ *
+ * <p><b>6. Typed input proto data</b> — direct access to typed protobuf input.
+ * Not accessed through this helper; the compiler generates typed cast + getter
+ * chains like {@code ((HTTPAccessLogEntry) h.ctx().input()).getResponse()}.
  *
  * <h2>Type Conversion Methods</h2>
  *
@@ -178,7 +183,7 @@ public final class LalRuntimeHelper {
      * </pre>
      */
     public String tagValue(final String key) {
-        final List dl = ctx.log().getTags().getDataList();
+        final List dl = ((LogData.Builder) ctx.input()).getTags().getDataList();
         for (int i = 0; i < dl.size(); i++) {
             final KeyStringValuePair kv = (KeyStringValuePair) dl.get(i);
             if (key.equals(kv.getKey())) {
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/DSL.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/DSL.java
index 265b674458..b279744a4c 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/DSL.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/DSL.java
@@ -20,10 +20,10 @@ package org.apache.skywalking.oap.log.analyzer.v2.dsl;
 import lombok.AccessLevel;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.skywalking.apm.network.logging.v3.LogData;
 import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALClassGenerator;
 import org.apache.skywalking.oap.log.analyzer.v2.dsl.spec.filter.FilterSpec;
 import org.apache.skywalking.oap.log.analyzer.v2.provider.LogAnalyzerModuleConfig;
+import org.apache.skywalking.oap.server.core.source.LogMetadata;
 import org.apache.skywalking.oap.server.library.module.ModuleManager;
 import org.apache.skywalking.oap.server.library.module.ModuleStartException;
 
@@ -82,11 +82,11 @@ public class DSL {
 
     public void evaluate(final ExecutionContext ctx) {
         if (log.isDebugEnabled()) {
-            final LogData.Builder logData = ctx.log();
-            log.debug("[LAL] rule={}, class={}, service={}, instance={}, endpoint={}, bodyType={}",
+            final LogMetadata metadata = ctx.metadata();
+            log.debug("[LAL] rule={}, class={}, service={}, instance={}, endpoint={}",
                 ruleName, expression.getClass().getName(),
-                logData.getService(), logData.getServiceInstance(),
-                logData.getEndpoint(), logData.getBody().getContentCase());
+                metadata.getService(), metadata.getServiceInstance(),
+                metadata.getEndpoint());
         }
         expression.execute(filterSpec, ctx);
     }
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/ExecutionContext.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/ExecutionContext.java
index 992d797720..04154077a2 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/ExecutionContext.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/ExecutionContext.java
@@ -23,10 +23,9 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.regex.Matcher;
 import lombok.Getter;
-import org.apache.skywalking.apm.network.logging.v3.LogData;
 import org.apache.skywalking.oap.meter.analyzer.v2.dsl.SampleFamily;
 import org.apache.skywalking.oap.server.core.source.LALOutputBuilder;
-import org.apache.skywalking.oap.server.core.source.Log;
+import org.apache.skywalking.oap.server.core.source.LogMetadata;
 
 /**
  * Mutable property storage for a single LAL script execution cycle.
@@ -34,21 +33,22 @@ import org.apache.skywalking.oap.server.core.source.Log;
  * <p>A new ExecutionContext is created for each incoming log. It carries all
  * per-log state through the compiled LAL pipeline:
  * <ul>
- *   <li>{@code log} — the incoming {@code LogData.Builder}</li>
+ *   <li>{@code metadata} — the {@link LogMetadata} (service, layer, timestamp, trace context)</li>
+ *   <li>{@code input} — the raw input object (LogData.Builder for standard logs, typed proto for ALS)</li>
  *   <li>{@code parsed} — structured data extracted by json/text/yaml parsers</li>
  *   <li>{@code save}/{@code abort} — control flags set by extractor/sink logic</li>
  *   <li>{@code metrics_container} — optional list for LAL-extracted metrics (log-MAL)</li>
- *   <li>{@code log_container} — optional container for the built {@code Log} source object</li>
+ *   <li>{@code dry_run} — when true, sink skips persistence (used by the LAL test tool)</li>
  * </ul>
  */
 public class ExecutionContext {
-    public static final String KEY_LOG = "log";
+    public static final String KEY_INPUT = "input";
+    public static final String KEY_METADATA = "metadata";
     public static final String KEY_PARSED = "parsed";
     public static final String KEY_SAVE = "save";
     public static final String KEY_ABORT = "abort";
     public static final String KEY_METRICS_CONTAINER = "metrics_container";
-    public static final String KEY_CAPTURE_LOG = "capture_log";
-    public static final String KEY_LOG_CONTAINER = "log_container";
+    public static final String KEY_DRY_RUN = "dry_run";
     public static final String KEY_OUTPUT = "output";
 
     private final Map<String, Object> properties = new HashMap<>();
@@ -65,32 +65,30 @@ public class ExecutionContext {
         return properties.get(name);
     }
 
-    public ExecutionContext log(final LogData.Builder log) {
-        setProperty(KEY_LOG, log);
+    /**
+     * Initialize from metadata + input.
+     */
+    public ExecutionContext init(final LogMetadata metadata, final Object input) {
+        setProperty(KEY_METADATA, metadata);
+        setProperty(KEY_INPUT, input);
         setProperty(KEY_SAVE, true);
         setProperty(KEY_ABORT, false);
         setProperty(KEY_METRICS_CONTAINER, null);
-        setProperty(KEY_CAPTURE_LOG, false);
-        setProperty(KEY_LOG_CONTAINER, null);
+        setProperty(KEY_DRY_RUN, false);
         setProperty(KEY_OUTPUT, null);
         return this;
     }
 
-    public ExecutionContext log(final LogData log) {
-        return log(log.toBuilder());
+    public LogMetadata metadata() {
+        return (LogMetadata) getProperty(KEY_METADATA);
     }
 
-    public LogData.Builder log() {
-        return (LogData.Builder) getProperty(KEY_LOG);
-    }
-
-    public ExecutionContext extraLog(final Object extraLog) {
-        parsed().extraLog = extraLog;
-        return this;
-    }
-
-    public Object extraLog() {
-        return parsed().getExtraLog();
+    /**
+     * Returns the raw input object. For standard logs this is a {@code LogData.Builder};
+     * for ALS it is the typed proto (e.g., {@code HTTPAccessLogEntry}).
+     */
+    public Object input() {
+        return getProperty(KEY_INPUT);
     }
 
     public ExecutionContext parsed(final Matcher parsed) {
@@ -140,22 +138,13 @@ public class ExecutionContext {
         return Optional.ofNullable((List<SampleFamily>) getProperty(KEY_METRICS_CONTAINER));
     }
 
-    public ExecutionContext captureLog(final boolean capture) {
-        setProperty(KEY_CAPTURE_LOG, capture);
-        return this;
-    }
-
-    public boolean shouldCaptureLog() {
-        return (boolean) getProperty(KEY_CAPTURE_LOG);
-    }
-
-    public ExecutionContext logContainer(final Log container) {
-        setProperty(KEY_LOG_CONTAINER, container);
+    public ExecutionContext dryRun(final boolean dryRun) {
+        setProperty(KEY_DRY_RUN, dryRun);
         return this;
     }
 
-    public Optional<Log> logContainer() {
-        return Optional.ofNullable((Log) getProperty(KEY_LOG_CONTAINER));
+    public boolean isDryRun() {
+        return (boolean) getProperty(KEY_DRY_RUN);
     }
 
     public void setOutput(final Object output) {
@@ -176,8 +165,5 @@ public class ExecutionContext {
 
         @Getter
         private Map<String, Object> map;
-
-        @Getter
-        private Object extraLog;
     }
 }
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/spec/filter/FilterSpec.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/spec/filter/FilterSpec.java
index 476a9802f4..dcd72fa052 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/spec/filter/FilterSpec.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/spec/filter/FilterSpec.java
@@ -19,11 +19,9 @@
 package org.apache.skywalking.oap.log.analyzer.v2.dsl.spec.filter;
 
 import com.fasterxml.jackson.core.type.TypeReference;
-import com.google.protobuf.TextFormat;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import org.apache.skywalking.apm.network.logging.v3.LogData;
 import org.apache.skywalking.oap.log.analyzer.v2.dsl.ExecutionContext;
 import org.apache.skywalking.oap.log.analyzer.v2.dsl.spec.AbstractSpec;
@@ -37,7 +35,7 @@ import org.apache.skywalking.oap.log.analyzer.v2.provider.LogAnalyzerModuleConfi
 import org.apache.skywalking.oap.log.analyzer.v2.provider.log.listener.LogSinkListenerFactory;
 import org.apache.skywalking.oap.log.analyzer.v2.provider.log.listener.RecordSinkListener;
 import org.apache.skywalking.oap.log.analyzer.v2.provider.log.listener.TrafficSinkListener;
-import org.apache.skywalking.oap.server.core.source.LogBuilder;
+import org.apache.skywalking.oap.server.core.source.LogMetadata;
 import org.apache.skywalking.oap.server.library.module.ModuleManager;
 import org.apache.skywalking.oap.server.library.module.ModuleStartException;
 import org.slf4j.Logger;
@@ -113,21 +111,21 @@ public class FilterSpec extends AbstractSpec {
     /**
      * LAL {@code json {}} — parses {@code LogData.body.json.json} into a
      * {@code Map<String, Object>} and stores it in {@code ctx.parsed()}.
-     * LogData proto fields (service, serviceInstance, endpoint, layer, timestamp)
+     * Metadata fields (service, serviceInstance, endpoint, layer, timestamp)
      * are also added to the map via {@code putIfAbsent}, so body values take
-     * priority while proto fields serve as fallback — matching v1 Groovy
+     * priority while metadata fields serve as fallback — matching v1 Groovy
      * {@code Binding.Parsed.getAt(key)} behavior.
      */
     public void json(final ExecutionContext ctx) {
         if (ctx.shouldAbort()) {
             return;
         }
-        final LogData.Builder logData = ctx.log();
         try {
+            final LogData.Builder logData = (LogData.Builder) ctx.input();
             final Map<String, Object> parsed = jsonParser.create().readValue(
                 logData.getBody().getJson().getJson(), parsedType
             );
-            addLogDataFields(parsed, logData);
+            addMetadataFields(parsed, ctx.metadata());
             ctx.parsed(parsed);
         } catch (final Exception e) {
             if (jsonParser.abortOnFailure()) {
@@ -139,18 +137,18 @@ public class FilterSpec extends AbstractSpec {
     /**
      * LAL {@code yaml {}} — parses {@code LogData.body.yaml.yaml} into a
      * {@code Map<String, Object>} and stores it in {@code ctx.parsed()}.
-     * LogData proto fields are added the same way as {@link #json(ExecutionContext)}.
+     * Metadata fields are added the same way as {@link #json(ExecutionContext)}.
      */
     public void yaml(final ExecutionContext ctx) {
         if (ctx.shouldAbort()) {
             return;
         }
-        final LogData.Builder logData = ctx.log();
         try {
+            final LogData.Builder logData = (LogData.Builder) ctx.input();
             final Map<String, Object> parsed = yamlParser.create().load(
                 logData.getBody().getYaml().getYaml()
             );
-            addLogDataFields(parsed, logData);
+            addMetadataFields(parsed, ctx.metadata());
             ctx.parsed(parsed);
         } catch (final Exception e) {
             if (yamlParser.abortOnFailure()) {
@@ -171,32 +169,24 @@ public class FilterSpec extends AbstractSpec {
     }
 
     private void doSink(final ExecutionContext ctx) {
-        final LogData.Builder logData = ctx.log();
-        final Optional<Object> extraLog = Optional.ofNullable(ctx.extraLog());
+        if (ctx.isDryRun()) {
+            return;
+        }
+
+        final LogMetadata metadata = ctx.metadata();
+        final Object input = ctx.input();
 
         if (!ctx.shouldSave()) {
             if (LOGGER.isDebugEnabled()) {
-                LOGGER.debug("Log is dropped: {}", TextFormat.shortDebugString(logData));
+                LOGGER.debug("Log is dropped: service={}, layer={}",
+                    metadata.getService(), metadata.getLayer());
             }
             return;
         }
 
-        if (ctx.shouldCaptureLog()) {
-            sinkListenerFactories.stream()
-                     .map(LogSinkListenerFactory::create)
-                     .filter(it -> it instanceof RecordSinkListener)
-                     .map(it -> it.parse(logData, extraLog, ctx))
-                     .map(it -> (RecordSinkListener) it)
-                     .map(RecordSinkListener::getBuilder)
-                     .filter(it -> it instanceof LogBuilder)
-                     .map(it -> ((LogBuilder) it).toLog())
-                     .findFirst()
-                     .ifPresent(log -> ctx.logContainer(log));
-        } else {
-            sinkListenerFactories.stream()
-                     .map(LogSinkListenerFactory::create)
-                     .forEach(it -> it.parse(logData, extraLog, ctx).build());
-        }
+        sinkListenerFactories.stream()
+                 .map(LogSinkListenerFactory::create)
+                 .forEach(it -> it.parse(metadata, input, ctx).build());
     }
 
     // ==================== Direct-access APIs for flattened generated code ====================
@@ -229,18 +219,21 @@ public class FilterSpec extends AbstractSpec {
     }
 
     /**
-     * Add LogData proto fields to the parsed map so that {@code parsed.service},
+     * Add metadata fields to the parsed map so that {@code parsed.service},
      * {@code parsed.serviceInstance}, etc. resolve correctly — matching v1 Groovy
      * {@code Binding.Parsed.getAt(key)} fallback behavior.
      * Uses {@code putIfAbsent} so body-parsed values take priority.
      */
-    private static void addLogDataFields(final Map<String, Object> parsed,
-                                         final LogData.Builder logData) {
-        putIfNotEmpty(parsed, "service", logData.getService());
-        putIfNotEmpty(parsed, "serviceInstance", logData.getServiceInstance());
-        putIfNotEmpty(parsed, "endpoint", logData.getEndpoint());
-        putIfNotEmpty(parsed, "layer", logData.getLayer());
-        final long ts = logData.getTimestamp();
+    private static void addMetadataFields(final Map<String, Object> parsed,
+                                          final LogMetadata metadata) {
+        if (metadata == null) {
+            return;
+        }
+        putIfNotEmpty(parsed, "service", metadata.getService());
+        putIfNotEmpty(parsed, "serviceInstance", metadata.getServiceInstance());
+        putIfNotEmpty(parsed, "endpoint", metadata.getEndpoint());
+        putIfNotEmpty(parsed, "layer", metadata.getLayer());
+        final long ts = metadata.getTimestamp();
         if (ts > 0) {
             parsed.putIfAbsent("timestamp", ts);
         }
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/spec/parser/TextParserSpec.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/spec/parser/TextParserSpec.java
index 8d57a0c2ad..a0f07926c2 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/spec/parser/TextParserSpec.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/spec/parser/TextParserSpec.java
@@ -39,7 +39,7 @@ public class TextParserSpec extends AbstractParserSpec {
         if (ctx.shouldAbort()) {
             return;
         }
-        final LogData.Builder log = ctx.log();
+        final LogData.Builder log = (LogData.Builder) ctx.input();
         final Matcher matcher = pattern.matcher(log.getBody().getText().getText());
         final boolean matched = matcher.find();
         if (matched) {
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/ILogAnalyzerService.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/ILogAnalyzerService.java
index ec951fba50..6aae340848 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/ILogAnalyzerService.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/ILogAnalyzerService.java
@@ -17,8 +17,7 @@
 
 package org.apache.skywalking.oap.log.analyzer.v2.provider.log;
 
-import java.util.Optional;
-import org.apache.skywalking.apm.network.logging.v3.LogData;
+import org.apache.skywalking.oap.server.core.source.LogMetadata;
 import org.apache.skywalking.oap.server.library.module.Service;
 
 /**
@@ -26,10 +25,11 @@ import org.apache.skywalking.oap.server.library.module.Service;
  */
 public interface ILogAnalyzerService extends Service {
 
-    void doAnalysis(LogData.Builder log, Optional<Object> extraLog);
-
-    default void doAnalysis(LogData logData, Optional<Object> extraLog) {
-        doAnalysis(logData.toBuilder(), extraLog);
-    }
+    /**
+     * @param metadata uniform metadata (service, layer, timestamp, trace context)
+     * @param input    source-specific input object (LogData for standard logs,
+     *                 HTTPAccessLogEntry for envoy ALS, etc.)
+     */
+    void doAnalysis(LogMetadata metadata, Object input);
 
 }
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/LogAnalyzer.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/LogAnalyzer.java
index 08f84a9156..19d99541bd 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/LogAnalyzer.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/LogAnalyzer.java
@@ -20,12 +20,11 @@ package org.apache.skywalking.oap.log.analyzer.v2.provider.log;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
-import java.util.Optional;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.skywalking.apm.network.logging.v3.LogData;
 import org.apache.skywalking.oap.log.analyzer.v2.provider.log.listener.LogAnalysisListener;
 import org.apache.skywalking.oap.server.core.UnexpectedException;
 import org.apache.skywalking.oap.server.core.analysis.Layer;
+import org.apache.skywalking.oap.server.core.source.LogMetadata;
 import org.apache.skywalking.oap.server.library.util.StringUtil;
 
 /**
@@ -40,9 +39,9 @@ import org.apache.skywalking.oap.server.library.util.StringUtil;
  *       {@link org.apache.skywalking.oap.log.analyzer.v2.provider.log.listener.LogFilterListener.Factory},
  *       which returns a listener wrapping all compiled {@link org.apache.skywalking.oap.log.analyzer.v2.dsl.DSL}
  *       instances for that layer.</li>
- *   <li>{@code notifyAnalysisListener(builder, extraLog)} — calls
+ *   <li>{@code notifyAnalysisListener(metadata, input)} — calls
  *       {@link org.apache.skywalking.oap.log.analyzer.v2.provider.log.listener.LogAnalysisListener#parse}
- *       on each listener, which binds the log data to the compiled LAL scripts.</li>
+ *       on each listener, which binds the metadata and input to the compiled LAL scripts.</li>
  *   <li>{@code notifyAnalysisListenerToBuild()} — calls
  *       {@link org.apache.skywalking.oap.log.analyzer.v2.provider.log.listener.LogAnalysisListener#build}
  *       on each listener, which evaluates the compiled LAL scripts (extractors, sinks).</li>
@@ -58,36 +57,34 @@ public class LogAnalyzer {
 
     private final List<LogAnalysisListener> listeners = new ArrayList<>();
 
-    public void doAnalysis(LogData.Builder builder, Optional<Object> extraLog) {
-        if (StringUtil.isEmpty(builder.getService())) {
-            // If the service name is empty, the log will be ignored.
+    public void doAnalysis(LogMetadata metadata, final Object input) {
+        if (StringUtil.isEmpty(metadata.getService())) {
             log.debug("The log is ignored because the Service name is empty");
             return;
         }
         Layer layer;
-        if ("".equals(builder.getLayer())) {
+        if (metadata.getLayer() == null || metadata.getLayer().isEmpty()) {
             layer = Layer.GENERAL;
         } else {
             try {
-                layer = Layer.nameOf(builder.getLayer());
+                layer = Layer.nameOf(metadata.getLayer());
             } catch (UnexpectedException e) {
-                log.warn("The Layer {} is not found, abandon the log.", builder.getLayer());
+                log.warn("The Layer {} is not found, abandon the log.", metadata.getLayer());
                 return;
             }
         }
 
         createAnalysisListeners(layer);
-        if (builder.getTimestamp() == 0) {
-            // If no timestamp, OAP server would use the received timestamp as log's timestamp
-            builder.setTimestamp(System.currentTimeMillis());
+        if (metadata.getTimestamp() == 0) {
+            metadata.setTimestamp(System.currentTimeMillis());
         }
 
-        notifyAnalysisListener(builder, extraLog);
+        notifyAnalysisListener(metadata, input);
         notifyAnalysisListenerToBuild();
     }
 
-    private void notifyAnalysisListener(LogData.Builder builder, final Optional<Object> extraLog) {
-        listeners.forEach(listener -> listener.parse(builder, extraLog));
+    private void notifyAnalysisListener(final LogMetadata metadata, final Object input) {
+        listeners.forEach(listener -> listener.parse(metadata, input));
     }
 
     private void notifyAnalysisListenerToBuild() {
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/LogAnalyzerServiceImpl.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/LogAnalyzerServiceImpl.java
index 8e1c746bfc..581a2a337d 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/LogAnalyzerServiceImpl.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/LogAnalyzerServiceImpl.java
@@ -19,11 +19,10 @@ package org.apache.skywalking.oap.log.analyzer.v2.provider.log;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Optional;
 import lombok.RequiredArgsConstructor;
-import org.apache.skywalking.apm.network.logging.v3.LogData;
 import org.apache.skywalking.oap.log.analyzer.v2.provider.LogAnalyzerModuleConfig;
 import org.apache.skywalking.oap.log.analyzer.v2.provider.log.listener.LogAnalysisListenerFactory;
+import org.apache.skywalking.oap.server.core.source.LogMetadata;
 import org.apache.skywalking.oap.server.library.module.ModuleManager;
 
 @RequiredArgsConstructor
@@ -33,9 +32,9 @@ public class LogAnalyzerServiceImpl implements ILogAnalyzerService, ILogAnalysis
     private final List<LogAnalysisListenerFactory> analysisListenerFactories = new ArrayList<>();
 
     @Override
-    public void doAnalysis(final LogData.Builder log, Optional<Object> extraLog) {
+    public void doAnalysis(final LogMetadata metadata, final Object input) {
         LogAnalyzer analyzer = new LogAnalyzer(this);
-        analyzer.doAnalysis(log, extraLog);
+        analyzer.doAnalysis(metadata, input);
     }
 
     @Override
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/LogAnalysisListener.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/LogAnalysisListener.java
index fe299835e9..0154ee76c6 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/LogAnalysisListener.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/LogAnalysisListener.java
@@ -17,8 +17,7 @@
 
 package org.apache.skywalking.oap.log.analyzer.v2.provider.log.listener;
 
-import java.util.Optional;
-import org.apache.skywalking.apm.network.logging.v3.LogData;
+import org.apache.skywalking.oap.server.core.source.LogMetadata;
 
 /**
  * LogAnalysisListener represents the callback when OAP does the log data analysis.
@@ -31,11 +30,10 @@ public interface LogAnalysisListener {
 
     /**
      * Parse the raw data from the probe.
-     * @param logData  log metadata (service, layer, timestamp, etc.)
-     * @param extraLog the actual input object whose type matches
-     *                 {@code LALSourceTypeProvider#inputType()} — may be {@code null}
-     *                 for standard logs where LogData is the sole input
+     * @param metadata uniform metadata (service, layer, timestamp, trace context)
+     * @param input    source-specific input object (LogData for standard logs,
+     *                 HTTPAccessLogEntry for envoy ALS, etc.)
      * @return {@code this} for chaining.
      */
-    LogAnalysisListener parse(LogData.Builder logData, Optional<Object> extraLog);
+    LogAnalysisListener parse(LogMetadata metadata, Object input);
 }
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/LogFilterListener.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/LogFilterListener.java
index 0d1873ac91..ab16b1f540 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/LogFilterListener.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/LogFilterListener.java
@@ -22,13 +22,11 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.ServiceLoader;
 import java.util.stream.Collectors;
 import lombok.extern.slf4j.Slf4j;
 
 import java.util.HashMap;
-import org.apache.skywalking.apm.network.logging.v3.LogData;
 import org.apache.skywalking.oap.log.analyzer.v2.dsl.ExecutionContext;
 import org.apache.skywalking.oap.log.analyzer.v2.dsl.DSL;
 import org.apache.skywalking.oap.log.analyzer.v2.provider.LALConfig;
@@ -38,6 +36,7 @@ import org.apache.skywalking.oap.log.analyzer.v2.spi.LALSourceTypeProvider;
 
 import org.apache.skywalking.oap.server.core.analysis.Layer;
 import org.apache.skywalking.oap.server.core.source.LALOutputBuilder;
+import org.apache.skywalking.oap.server.core.source.LogMetadata;
 import org.apache.skywalking.oap.server.library.module.ModuleManager;
 import org.apache.skywalking.oap.server.library.module.ModuleStartException;
 
@@ -76,12 +75,11 @@ public class LogFilterListener implements LogAnalysisListener {
     }
 
     @Override
-    public LogAnalysisListener parse(final LogData.Builder logData,
-                                     final Optional<Object> extraLog) {
-        final LogData logDataSnapshot = logData.build();
+    public LogAnalysisListener parse(final LogMetadata metadata,
+                                     final Object input) {
         contexts = new ArrayList<>(dsls.size());
         for (int i = 0; i < dsls.size(); i++) {
-            contexts.add(new ExecutionContext().log(logDataSnapshot).extraLog(extraLog.orElse(null)));
+            contexts.add(new ExecutionContext().init(metadata, input));
         }
         return this;
     }
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/LogSinkListener.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/LogSinkListener.java
index afda6f0671..d282156505 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/LogSinkListener.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/LogSinkListener.java
@@ -17,9 +17,8 @@
 
 package org.apache.skywalking.oap.log.analyzer.v2.provider.log.listener;
 
-import java.util.Optional;
-import org.apache.skywalking.apm.network.logging.v3.LogData;
 import org.apache.skywalking.oap.log.analyzer.v2.dsl.ExecutionContext;
+import org.apache.skywalking.oap.server.core.source.LogMetadata;
 
 public interface LogSinkListener {
     /**
@@ -32,16 +31,5 @@ public interface LogSinkListener {
      * Parse the raw data from the probe.
      * @return {@code this} for chaining.
      */
-    LogSinkListener parse(LogData.Builder logData, Optional<Object> extraLog);
-
-    /**
-     * Parse the raw data from the probe with access to the execution context.
-     * Implementations can use the context to apply output fields or other
-     * per-execution state to the sink output.
-     * @return {@code this} for chaining.
-     */
-    default LogSinkListener parse(final LogData.Builder logData, final Optional<Object> extraLog,
-                                  final ExecutionContext ctx) {
-        return parse(logData, extraLog);
-    }
+    LogSinkListener parse(LogMetadata metadata, Object input, ExecutionContext ctx);
 }
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/RecordSinkListener.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/RecordSinkListener.java
index 11e741db7d..4d6e465e2b 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/RecordSinkListener.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/RecordSinkListener.java
@@ -17,14 +17,13 @@
 
 package org.apache.skywalking.oap.log.analyzer.v2.provider.log.listener;
 
-import java.util.Optional;
 import lombok.SneakyThrows;
-import org.apache.skywalking.apm.network.logging.v3.LogData;
 
 import org.apache.skywalking.oap.log.analyzer.v2.dsl.ExecutionContext;
 import org.apache.skywalking.oap.log.analyzer.v2.provider.LogAnalyzerModuleConfig;
 import org.apache.skywalking.oap.server.core.CoreModule;
 import org.apache.skywalking.oap.server.core.source.LALOutputBuilder;
+import org.apache.skywalking.oap.server.core.source.LogMetadata;
 import org.apache.skywalking.oap.server.core.source.SourceReceiver;
 import org.apache.skywalking.oap.server.library.module.ModuleManager;
 import org.slf4j.Logger;
@@ -34,7 +33,7 @@ import org.slf4j.LoggerFactory;
  * RecordSinkListener forwards LAL output to the persistence layer.
  *
  * <p>All LAL rules produce an {@link LALOutputBuilder} in the {@link ExecutionContext}.
- * This listener calls {@code init()} to populate standard fields from LogData,
+ * This listener calls {@code init()} to populate standard fields from metadata,
  * then {@code complete()} to dispatch the final source(s).
  */
 public class RecordSinkListener implements LogSinkListener {
@@ -64,23 +63,14 @@ public class RecordSinkListener implements LogSinkListener {
 
     @Override
     @SneakyThrows
-    public LogSinkListener parse(final LogData.Builder logData,
-                                     final Optional<Object> extraLog) {
-        return this;
-    }
-
-    @Override
-    @SneakyThrows
-    public LogSinkListener parse(final LogData.Builder logData,
-                                 final Optional<Object> extraLog,
+    public LogSinkListener parse(final LogMetadata metadata,
+                                 final Object input,
                                  final ExecutionContext ctx) {
         if (ctx == null || !(ctx.output() instanceof LALOutputBuilder)) {
             return this;
         }
         builder = ctx.outputAsBuilder();
-        // Pass the input data matching the declared inputType:
-        // extraLog (e.g., HTTPAccessLogEntry) when present, otherwise LogData.
-        builder.init(logData.build(), extraLog, moduleManager);
+        builder.init(metadata, input, moduleManager);
         return this;
     }
 
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/TrafficSinkListener.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/TrafficSinkListener.java
index 8a9ab2b6f2..9679a3354f 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/TrafficSinkListener.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/TrafficSinkListener.java
@@ -17,10 +17,10 @@
 
 package org.apache.skywalking.oap.log.analyzer.v2.provider.log.listener;
 
-import java.util.Optional;
 import lombok.RequiredArgsConstructor;
-import org.apache.skywalking.apm.network.logging.v3.LogData;
+import org.apache.skywalking.oap.log.analyzer.v2.dsl.ExecutionContext;
 import org.apache.skywalking.oap.server.core.analysis.Layer;
+import org.apache.skywalking.oap.server.core.source.LogMetadata;
 import org.apache.skywalking.oap.server.library.util.StringUtil;
 import org.apache.skywalking.oap.log.analyzer.v2.provider.LogAnalyzerModuleConfig;
 import org.apache.skywalking.oap.server.core.CoreModule;
@@ -62,36 +62,37 @@ public class TrafficSinkListener implements LogSinkListener {
     }
 
     @Override
-    public LogSinkListener parse(final LogData.Builder logData,
-                                     final Optional<Object> extraLog) {
+    public LogSinkListener parse(final LogMetadata metadata,
+                                 final Object input,
+                                 final ExecutionContext ctx) {
         Layer layer;
-        if (StringUtil.isNotEmpty(logData.getLayer())) {
-            layer = Layer.valueOf(logData.getLayer());
+        if (StringUtil.isNotEmpty(metadata.getLayer())) {
+            layer = Layer.valueOf(metadata.getLayer());
         } else {
             layer = Layer.GENERAL;
         }
         final long timeBucket = TimeBucket.getTimeBucket(System.currentTimeMillis(), DownSampling.Minute);
         // to service traffic
-        String serviceName = namingControl.formatServiceName(logData.getService());
+        String serviceName = namingControl.formatServiceName(metadata.getService());
         String serviceId = IDManager.ServiceID.buildId(serviceName, layer.isNormal());
         serviceMeta = new ServiceMeta();
-        serviceMeta.setName(namingControl.formatServiceName(logData.getService()));
+        serviceMeta.setName(namingControl.formatServiceName(metadata.getService()));
         serviceMeta.setLayer(layer);
         serviceMeta.setTimeBucket(timeBucket);
         // to service instance traffic
-        if (StringUtil.isNotEmpty(logData.getServiceInstance())) {
+        if (StringUtil.isNotEmpty(metadata.getServiceInstance())) {
             instanceMeta = new ServiceInstanceUpdate();
             instanceMeta.setServiceId(serviceId);
-            instanceMeta.setName(namingControl.formatInstanceName(logData.getServiceInstance()));
+            instanceMeta.setName(namingControl.formatInstanceName(metadata.getServiceInstance()));
             instanceMeta.setTimeBucket(timeBucket);
 
         }
         // to endpoint traffic
-        if (StringUtil.isNotEmpty(logData.getEndpoint())) {
+        if (StringUtil.isNotEmpty(metadata.getEndpoint())) {
             endpointMeta = new EndpointMeta();
             endpointMeta.setServiceName(serviceName);
             endpointMeta.setServiceNormal(true);
-            endpointMeta.setEndpoint(namingControl.formatEndpointName(serviceName, logData.getEndpoint()));
+            endpointMeta.setEndpoint(namingControl.formatEndpointName(serviceName, metadata.getEndpoint()));
             endpointMeta.setTimeBucket(timeBucket);
         }
         return this;
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/spi/LALSourceTypeProvider.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/spi/LALSourceTypeProvider.java
index d843b9159c..f97f56c044 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/spi/LALSourceTypeProvider.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/spi/LALSourceTypeProvider.java
@@ -23,8 +23,8 @@ import org.apache.skywalking.oap.server.core.analysis.Layer;
  * SPI for receiver plugins to declare the input and default output types for
  * LAL rules on a specific {@link Layer}.
  *
- * <p><b>Input type</b> ({@link #inputType()}): The Java type of the {@code extraLog}
- * passed to LAL via {@code ILogAnalyzerService.doAnalysis(LogData, Message)}.
+ * <p><b>Input type</b> ({@link #inputType()}): The Java type of the {@code input}
+ * passed to LAL via {@code ILogAnalyzerService.doAnalysis(LogMetadata, Object)}.
  * The LAL compiler uses this at compile time to generate optimized direct
  * getter calls for {@code parsed.*} field access. This is per-layer because
  * all rules for a layer share the same input proto type.
@@ -42,7 +42,7 @@ import org.apache.skywalking.oap.server.core.analysis.Layer;
  *   <li>DSL parser ({@code json{}}, {@code yaml{}}, {@code text{}}) wins over all</li>
  *   <li>Explicit {@code inputType} declared in the YAML rule config</li>
  *   <li>This SPI — acts as the default for a layer</li>
- *   <li>{@code LogData.Builder} fallback if none of the above</li>
+ *   <li>{@code LogMetadata} fallback if none of the above</li>
  * </ol>
  *
  * <p>Output type resolution order:
@@ -62,7 +62,7 @@ public interface LALSourceTypeProvider {
     Layer layer();
 
     /**
-     * The Java type passed as {@code extraLog} by the receiver plugin for
+     * The Java type passed as {@code input} by the receiver plugin for
      * this layer. The compiler resolves getter chains on this type at
      * compile time.
      */
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
index 8fa30a6adc..8fa8640ed8 100644
--- 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
@@ -117,7 +117,7 @@ class LALClassGeneratorExtractorTest extends LALClassGeneratorTestBase {
     }
 
     @Test
-    void compileNoParserFallsBackToLogDataProto() throws Exception {
+    void compileNoParserFallsBackToLogMetadata() throws Exception {
         final String dsl =
             "filter {\n"
             + "  extractor {\n"
@@ -127,12 +127,12 @@ class LALClassGeneratorExtractorTest extends LALClassGeneratorTestBase {
             + "  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);
+        assertTrue(source.contains("h.ctx().metadata().getService()"),
+            "Expected h.ctx().metadata().getService() but got: " + source);
+        assertTrue(source.contains("h.ctx().metadata().getServiceInstance()"),
+            "Expected h.ctx().metadata().getServiceInstance() but got: " + source);
         assertFalse(source.contains("_p"),
-            "Should NOT have _p variable for LogData fallback but got: " + source);
+            "Should NOT have _p variable for LogMetadata fallback but got: " + source);
         compileAndAssert(dsl);
     }
 
@@ -158,7 +158,7 @@ class LALClassGeneratorExtractorTest extends LALClassGeneratorTestBase {
         final String fqcn =
             "io.envoyproxy.envoy.data.accesslog.v3.HTTPAccessLogEntry";
         assertTrue(source.contains(
-            fqcn + " _p = (" + fqcn + ") h.ctx().extraLog()"),
+            fqcn + " _p = (" + fqcn + ") h.ctx().input()"),
             "Expected _p local variable for inputType cast but got: " + source);
         assertTrue(source.contains("_p.getResponse()"),
             "Expected _p.getResponse() via cached variable but got: " + source);
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 38e51069d5..07004a4d6f 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
@@ -39,6 +39,8 @@ 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.source.LogMetadata;
+import org.apache.skywalking.oap.server.core.source.LogMetadataUtils;
 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.config.ConfigService;
@@ -173,13 +175,13 @@ class LALExpressionExecutionTest {
         if (ruleLayer != null) {
             logData.setLayer(ruleLayer);
         }
-        final ExecutionContext ctx = new ExecutionContext();
-        ctx.log(logData);
+        final LogMetadata metadata = LogMetadataUtils.fromLogData(logData);
 
         final Message extraLog = LalLogDataBuilder.buildExtraLog(input);
-        if (extraLog != null) {
-            ctx.extraLog(extraLog);
-        }
+        final Object lalInput = extraLog != null ? extraLog : logData;
+
+        final ExecutionContext ctx = new ExecutionContext();
+        ctx.init(metadata, lalInput);
 
         expr.execute(filterSpec, ctx);
 
@@ -208,28 +210,28 @@ class LALExpressionExecutionTest {
                         ruleName + ": shouldAbort mismatch");
                     break;
                 case "service":
-                    assertOutputField(ruleName, outputObj, "service", expected, ctx.log().getService());
+                    assertOutputField(ruleName, outputObj, "service", expected, ctx.metadata().getService());
                     break;
                 case "instance":
-                    assertOutputField(ruleName, outputObj, "serviceInstance", expected, ctx.log().getServiceInstance());
+                    assertOutputField(ruleName, outputObj, "serviceInstance", expected, ctx.metadata().getServiceInstance());
                     break;
                 case "endpoint":
-                    assertOutputField(ruleName, outputObj, "endpoint", expected, ctx.log().getEndpoint());
+                    assertOutputField(ruleName, outputObj, "endpoint", expected, ctx.metadata().getEndpoint());
                     break;
                 case "layer":
-                    assertOutputField(ruleName, outputObj, "layer", expected, ctx.log().getLayer());
+                    assertOutputField(ruleName, outputObj, "layer", expected, ctx.metadata().getLayer());
                     break;
                 case "timestamp":
-                    assertOutputField(ruleName, outputObj, "timestamp", expected, String.valueOf(ctx.log().getTimestamp()));
+                    assertOutputField(ruleName, outputObj, "timestamp", expected, String.valueOf(ctx.metadata().getTimestamp()));
                     break;
                 default:
                     if (key.startsWith("tag.")) {
                         final String tagKey = key.substring(4);
                         if (outputObj instanceof org.apache.skywalking.oap.server.core.source.LogBuilder) {
                             assertLalTag(ruleName, outputObj, tagKey, expected);
-                        } else {
+                        } else if (ctx.input() instanceof LogData.Builder) {
                             final List<KeyStringValuePair> tags =
-                                ctx.log().getTags().getDataList();
+                                ((LogData.Builder) ctx.input()).getTags().getDataList();
                             assertTrue(tags.stream().anyMatch(
                                 t -> tagKey.equals(t.getKey())
                                     && expected.equals(t.getValue())),
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/LALOutputBuilder.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/LALOutputBuilder.java
index fad33dd9a8..e88ab85368 100644
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/LALOutputBuilder.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/LALOutputBuilder.java
@@ -18,8 +18,6 @@
 
 package org.apache.skywalking.oap.server.core.source;
 
-import java.util.Optional;
-import org.apache.skywalking.apm.network.logging.v3.LogData;
 import org.apache.skywalking.oap.server.library.module.ModuleManager;
 
 /**
@@ -53,13 +51,12 @@ public interface LALOutputBuilder {
      * Pre-populate standard fields before custom output fields are applied.
      * Called once per log entry.
      *
-     * @param logData  log metadata (service, layer, timestamp, trace context, etc.)
-     * @param extraLog      optional extra input whose type matches
-     *                      {@code LALSourceTypeProvider#inputType()} for the layer
-     *                      (e.g., {@code HTTPAccessLogEntry} for envoy access logs)
+     * @param metadata      uniform metadata (service, layer, timestamp, trace context, etc.)
+     * @param input         source-specific input object ({@code LogData} for standard logs,
+     *                      {@code HTTPAccessLogEntry} for envoy access logs, etc.)
      * @param moduleManager module manager for resolving services (e.g., NamingControl)
      */
-    void init(LogData logData, Optional<Object> extraLog, ModuleManager moduleManager);
+    void init(LogMetadata metadata, Object input, ModuleManager moduleManager);
 
     /**
      * Validate the builder state and dispatch the final output source(s).
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/LogBuilder.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/LogBuilder.java
index 96f7f7a294..a49ffc83cb 100644
--- a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/LogBuilder.java
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/LogBuilder.java
@@ -23,7 +23,6 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Optional;
 import java.util.UUID;
 import lombok.Getter;
 import lombok.Setter;
@@ -31,7 +30,6 @@ import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.skywalking.apm.network.logging.v3.LogData;
 import org.apache.skywalking.apm.network.logging.v3.LogDataBody;
-import org.apache.skywalking.apm.network.logging.v3.TraceContext;
 import org.apache.skywalking.oap.server.core.Const;
 import org.apache.skywalking.oap.server.core.CoreModule;
 import org.apache.skywalking.oap.server.core.analysis.IDManager;
@@ -95,8 +93,21 @@ public class LogBuilder implements LALOutputBuilder {
     }
 
     @Override
-    public void init(final LogData logData, final Optional<Object> extraLog,
+    public void init(final LogMetadata metadata, final Object input,
                      final ModuleManager moduleManager) {
+        ensureInitialized(moduleManager);
+        if (input instanceof LogData) {
+            this.logData = (LogData) input;
+        } else if (input instanceof LogData.Builder) {
+            this.logData = ((LogData.Builder) input).build();
+        }
+        initFromMetadata(metadata);
+    }
+
+    /**
+     * Initialize static services from ModuleManager (once per JVM).
+     */
+    protected void ensureInitialized(final ModuleManager moduleManager) {
         if (!INITIALIZED) {
             NAMING_CONTROL = moduleManager.find(CoreModule.NAME)
                                           .provider()
@@ -108,33 +119,39 @@ public class LogBuilder implements LALOutputBuilder {
                 configService.getSearchableLogsTags().split(Const.COMMA));
             INITIALIZED = true;
         }
-        this.logData = logData;
-        // Only populate fields that were NOT already set by the LAL extractor.
-        // The extractor runs before init(), so extractor values take priority.
+    }
+
+    /**
+     * Populate fields from metadata. Only sets fields not already set by
+     * the LAL extractor (extractor runs before init, so its values take priority).
+     */
+    protected void initFromMetadata(final LogMetadata metadata) {
         if (this.service == null) {
-            this.service = logData.getService();
+            this.service = metadata.getService();
         }
         if (this.serviceInstance == null) {
-            this.serviceInstance = logData.getServiceInstance();
+            this.serviceInstance = metadata.getServiceInstance();
         }
         if (this.endpoint == null) {
-            this.endpoint = logData.getEndpoint();
+            this.endpoint = metadata.getEndpoint();
         }
         if (this.layer == null) {
-            this.layer = logData.getLayer();
-        }
-        final TraceContext tc = logData.getTraceContext();
-        if (this.traceId == null) {
-            this.traceId = tc.getTraceId();
-        }
-        if (this.segmentId == null) {
-            this.segmentId = tc.getTraceSegmentId();
+            this.layer = metadata.getLayer();
         }
-        if (this.spanId < 0) {
-            this.spanId = tc.getSpanId();
+        final LogMetadata.TraceContext tc = metadata.getTraceContext();
+        if (tc != null) {
+            if (this.traceId == null) {
+                this.traceId = tc.getTraceId();
+            }
+            if (this.segmentId == null) {
+                this.segmentId = tc.getTraceSegmentId();
+            }
+            if (this.spanId < 0) {
+                this.spanId = tc.getSpanId();
+            }
         }
         if (this.timestamp == 0) {
-            this.timestamp = logData.getTimestamp();
+            this.timestamp = metadata.getTimestamp();
         }
     }
 
@@ -179,21 +196,23 @@ public class LogBuilder implements LALOutputBuilder {
                 log.setSpanId(spanId);
             }
         }
-        // content
-        final LogDataBody body = logData.getBody();
-        if (body.hasText()) {
-            log.setContentType(ContentType.TEXT);
-            log.setContent(body.getText().getText());
-        } else if (body.hasYaml()) {
-            log.setContentType(ContentType.YAML);
-            log.setContent(body.getYaml().getYaml());
-        } else if (body.hasJson()) {
-            log.setContentType(ContentType.JSON);
-            log.setContent(body.getJson().getJson());
-        }
-        // raw tags from original LogData
-        if (logData.getTags().getDataCount() > 0) {
-            log.setTagsRawData(logData.getTags().toByteArray());
+        // content (only when input is LogData)
+        if (logData != null) {
+            final LogDataBody body = logData.getBody();
+            if (body.hasText()) {
+                log.setContentType(ContentType.TEXT);
+                log.setContent(body.getText().getText());
+            } else if (body.hasYaml()) {
+                log.setContentType(ContentType.YAML);
+                log.setContent(body.getYaml().getYaml());
+            } else if (body.hasJson()) {
+                log.setContentType(ContentType.JSON);
+                log.setContent(body.getJson().getJson());
+            }
+            // raw tags from original LogData
+            if (logData.getTags().getDataCount() > 0) {
+                log.setTagsRawData(logData.getTags().toByteArray());
+            }
         }
         // searchable tags from LogData + LAL-added tags
         log.getTags().addAll(collectSearchableTags());
@@ -205,11 +224,13 @@ public class LogBuilder implements LALOutputBuilder {
         final HashSet<Tag> result = new HashSet<>();
         if (SEARCHABLE_TAG_KEYS != null) {
             // Tags from original LogData
-            logData.getTags().getDataList().forEach(kv -> {
-                if (SEARCHABLE_TAG_KEYS.contains(kv.getKey())) {
-                    addSearchableTag(result, kv.getKey(), kv.getValue());
-                }
-            });
+            if (logData != null) {
+                logData.getTags().getDataList().forEach(kv -> {
+                    if (SEARCHABLE_TAG_KEYS.contains(kv.getKey())) {
+                        addSearchableTag(result, kv.getKey(), kv.getValue());
+                    }
+                });
+            }
             // Tags added by LAL extractor
             for (final String[] kv : lalTags) {
                 if (SEARCHABLE_TAG_KEYS.contains(kv[0])) {
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/LogMetadata.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/LogMetadata.java
new file mode 100644
index 0000000000..5c029fb74a
--- /dev/null
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/LogMetadata.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.server.core.source;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * Uniform metadata carrier for log analysis. Receivers build this from their
+ * native source (LogData proto for standard logs, ALS context for envoy, etc.).
+ *
+ * <p>For building from {@code LogData} protobuf, see {@link LogMetadataUtils}.
+ */
+@Data
+@Builder
+public class LogMetadata {
+    private String service;
+    private String serviceInstance;
+    private String endpoint;
+    private String layer;
+    private long timestamp;
+    @Builder.Default
+    private TraceContext traceContext = TraceContext.EMPTY;
+
+    @Data
+    @Builder
+    public static class TraceContext {
+        static final TraceContext EMPTY =
+            TraceContext.builder().traceId("").traceSegmentId("").spanId(0).build();
+
+        @Builder.Default
+        private String traceId = "";
+        @Builder.Default
+        private String traceSegmentId = "";
+        private int spanId;
+    }
+}
diff --git a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/LogMetadataUtils.java b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/LogMetadataUtils.java
new file mode 100644
index 0000000000..d0420fee04
--- /dev/null
+++ b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/LogMetadataUtils.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.server.core.source;
+
+import org.apache.skywalking.apm.network.logging.v3.LogData;
+import org.apache.skywalking.apm.network.logging.v3.TraceContext;
+
+/**
+ * Utility for building {@link LogMetadata} from protobuf {@link LogData}.
+ */
+public final class LogMetadataUtils {
+
+    private LogMetadataUtils() {
+    }
+
+    /**
+     * Build LogMetadata from a LogData protobuf message.
+     */
+    public static LogMetadata fromLogData(final LogData logData) {
+        final LogMetadata.TraceContext tc = buildTraceContext(logData.getTraceContext());
+        return LogMetadata.builder()
+                          .service(logData.getService())
+                          .serviceInstance(logData.getServiceInstance())
+                          .endpoint(logData.getEndpoint())
+                          .layer(logData.getLayer())
+                          .timestamp(logData.getTimestamp())
+                          .traceContext(tc)
+                          .build();
+    }
+
+    /**
+     * Build LogMetadata from a LogData.Builder without building a full LogData.
+     */
+    public static LogMetadata fromLogData(final LogData.Builder builder) {
+        final LogMetadata.TraceContext tc = buildTraceContext(builder.getTraceContext());
+        return LogMetadata.builder()
+                          .service(builder.getService())
+                          .serviceInstance(builder.getServiceInstance())
+                          .endpoint(builder.getEndpoint())
+                          .layer(builder.getLayer())
+                          .timestamp(builder.getTimestamp())
+                          .traceContext(tc)
+                          .build();
+    }
+
+    private static LogMetadata.TraceContext buildTraceContext(final TraceContext ctxProto) {
+        if (ctxProto == null || ctxProto == TraceContext.getDefaultInstance()) {
+            return LogMetadata.TraceContext.EMPTY;
+        }
+        return LogMetadata.TraceContext.builder()
+                                      .traceId(ctxProto.getTraceId())
+                                      .traceSegmentId(ctxProto.getTraceSegmentId())
+                                      .spanId(ctxProto.getSpanId())
+                                      .build();
+    }
+}
diff --git a/oap-server/server-fetcher-plugin/kafka-fetcher-plugin/src/main/java/org/apache/skywalking/oap/server/analyzer/agent/kafka/provider/handler/LogHandler.java b/oap-server/server-fetcher-plugin/kafka-fetcher-plugin/src/main/java/org/apache/skywalking/oap/server/analyzer/agent/kafka/provider/handler/LogHandler.java
index c39925fad5..18e1ca6351 100644
--- a/oap-server/server-fetcher-plugin/kafka-fetcher-plugin/src/main/java/org/apache/skywalking/oap/server/analyzer/agent/kafka/provider/handler/LogHandler.java
+++ b/oap-server/server-fetcher-plugin/kafka-fetcher-plugin/src/main/java/org/apache/skywalking/oap/server/analyzer/agent/kafka/provider/handler/LogHandler.java
@@ -20,8 +20,8 @@ package org.apache.skywalking.oap.server.analyzer.agent.kafka.provider.handler;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.kafka.clients.consumer.ConsumerRecord;
 import org.apache.kafka.common.utils.Bytes;
-import java.util.Optional;
 import org.apache.skywalking.apm.network.logging.v3.LogData;
+import org.apache.skywalking.oap.server.core.source.LogMetadataUtils;
 import org.apache.skywalking.oap.log.analyzer.v2.module.LogAnalyzerModule;
 import org.apache.skywalking.oap.log.analyzer.v2.provider.log.ILogAnalyzerService;
 import org.apache.skywalking.oap.server.analyzer.agent.kafka.module.KafkaFetcherConfig;
@@ -71,8 +71,8 @@ public class LogHandler extends AbstractKafkaHandler {
     @Override
     public void handle(final ConsumerRecord<String, Bytes> record) {
         try (HistogramMetrics.Timer ignore = histogram.createTimer()) {
-            LogData logData = parseConsumerRecord(record);
-            logAnalyzerService.doAnalysis(logData, Optional.empty());
+            LogData.Builder builder = parseConsumerRecord(record).toBuilder();
+            logAnalyzerService.doAnalysis(LogMetadataUtils.fromLogData(builder), builder);
         } catch (Exception e) {
             errorCounter.inc();
             log.error(e.getMessage(), e);
diff --git a/oap-server/server-query-plugin/query-graphql-plugin/src/main/java/org/apache/skywalking/oap/query/graphql/resolver/LogTestQuery.java b/oap-server/server-query-plugin/query-graphql-plugin/src/main/java/org/apache/skywalking/oap/query/graphql/resolver/LogTestQuery.java
index 8e3b481208..461fd62080 100644
--- a/oap-server/server-query-plugin/query-graphql-plugin/src/main/java/org/apache/skywalking/oap/query/graphql/resolver/LogTestQuery.java
+++ b/oap-server/server-query-plugin/query-graphql-plugin/src/main/java/org/apache/skywalking/oap/query/graphql/resolver/LogTestQuery.java
@@ -29,6 +29,7 @@ import org.apache.skywalking.apm.network.logging.v3.LogData;
 import org.apache.skywalking.apm.network.logging.v3.LogTags;
 import org.apache.skywalking.oap.log.analyzer.v2.dsl.ExecutionContext;
 import org.apache.skywalking.oap.log.analyzer.v2.dsl.DSL;
+import org.apache.skywalking.oap.server.core.source.LogMetadataUtils;
 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;
@@ -39,6 +40,8 @@ import org.apache.skywalking.oap.query.graphql.type.Metrics;
 import org.apache.skywalking.oap.server.core.analysis.IDManager;
 import org.apache.skywalking.oap.server.core.query.type.KeyValue;
 import org.apache.skywalking.oap.server.core.query.type.Log;
+import org.apache.skywalking.oap.server.core.source.LogBuilder;
+import org.apache.skywalking.oap.server.core.source.LogMetadata;
 import org.apache.skywalking.oap.server.library.module.ModuleManager;
 import org.apache.skywalking.oap.server.library.util.ProtoBufJsonUtils;
 
@@ -72,17 +75,21 @@ public class LogTestQuery implements GraphQLQueryResolver {
 
         final LogData.Builder log = LogData.newBuilder();
         ProtoBufJsonUtils.fromJSON(request.getLog(), log);
-        ctx.log(log);
+        final LogMetadata metadata = LogMetadataUtils.fromLogData(log);
+        ctx.init(metadata, log);
 
-        ctx.captureLog(true);
+        ctx.dryRun(true);
         ctx.metricsContainer(new ArrayList<>());
 
         dsl.evaluate(ctx);
 
         final LogTestResponse.LogTestResponseBuilder builder = LogTestResponse.builder();
-        ctx.logContainer().ifPresent(it -> {
-            final Log l = new Log();
+        if (ctx.shouldSave() && ctx.outputAsBuilder() instanceof LogBuilder) {
+            final LogBuilder logBuilder = (LogBuilder) ctx.outputAsBuilder();
+            logBuilder.init(metadata, log, moduleManager);
+            final org.apache.skywalking.oap.server.core.source.Log it = logBuilder.toLog();
 
+            final Log l = new Log();
             if (isNotBlank(it.getServiceId())) {
                 l.setServiceName(IDManager.ServiceID.analysisId(it.getServiceId()).getName());
             }
@@ -113,9 +120,8 @@ public class LogTestQuery implements GraphQLQueryResolver {
                     // ignore
                 }
             }
-
             builder.log(l);
-        });
+        }
         ctx.metricsContainer().ifPresent(it -> {
             final List<Metrics> samples =
                 it.stream()
diff --git a/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/EnvoyAccessLogBuilder.java b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/EnvoyAccessLogBuilder.java
index 60ba08618c..9478939481 100644
--- a/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/EnvoyAccessLogBuilder.java
+++ b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/EnvoyAccessLogBuilder.java
@@ -19,12 +19,11 @@
 package org.apache.skywalking.oap.server.receiver.envoy.persistence;
 
 import com.google.protobuf.Message;
-import java.util.Optional;
 import lombok.SneakyThrows;
-import org.apache.skywalking.apm.network.logging.v3.LogData;
 import org.apache.skywalking.oap.server.core.query.type.ContentType;
 import org.apache.skywalking.oap.server.core.source.Log;
 import org.apache.skywalking.oap.server.core.source.LogBuilder;
+import org.apache.skywalking.oap.server.core.source.LogMetadata;
 import org.apache.skywalking.oap.server.library.module.ModuleManager;
 import org.apache.skywalking.oap.server.library.util.ProtoBufJsonUtils;
 
@@ -35,9 +34,9 @@ import org.apache.skywalking.oap.server.library.util.ProtoBufJsonUtils;
  * as JSON content. The serialization is deferred to {@link #toLog()} so that
  * it only happens when the log is actually persisted (after LAL filtering).
  *
- * <p>The {@link #init} method stores the extra log entry (e.g.,
- * {@code HTTPAccessLogEntry}) for JSON serialization, then delegates to
- * the base class for standard field population from {@code LogData}.
+ * <p>The {@link #init} method stores the access log entry for JSON
+ * serialization, then delegates to the base class for metadata-only
+ * field population (no LogData in the envoy path).
  */
 public class EnvoyAccessLogBuilder extends LogBuilder {
     public static final String NAME = "EnvoyAccessLog";
@@ -50,10 +49,13 @@ public class EnvoyAccessLogBuilder extends LogBuilder {
     }
 
     @Override
-    public void init(final LogData logData, final Optional<Object> extraLog,
+    public void init(final LogMetadata metadata, final Object input,
                      final ModuleManager moduleManager) {
-        extraLog.ifPresent(entry -> this.accessLogEntry = entry);
-        super.init(logData, extraLog, moduleManager);
+        if (input != null) {
+            this.accessLogEntry = input;
+        }
+        ensureInitialized(moduleManager);
+        initFromMetadata(metadata);
     }
 
     @Override
diff --git a/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/LogsPersistence.java b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/LogsPersistence.java
index 09437cbe3b..f2ac92c15f 100644
--- a/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/LogsPersistence.java
+++ b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/LogsPersistence.java
@@ -20,13 +20,12 @@ package org.apache.skywalking.oap.server.receiver.envoy.persistence;
 
 import io.envoyproxy.envoy.data.accesslog.v3.HTTPAccessLogEntry;
 import io.envoyproxy.envoy.service.accesslog.v3.StreamAccessLogsMessage;
-import java.util.Optional;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.skywalking.apm.network.logging.v3.LogData;
 import org.apache.skywalking.apm.network.servicemesh.v3.HTTPServiceMeshMetric;
 import org.apache.skywalking.oap.log.analyzer.v2.module.LogAnalyzerModule;
 import org.apache.skywalking.oap.log.analyzer.v2.provider.log.ILogAnalyzerService;
 import org.apache.skywalking.oap.server.core.analysis.Layer;
+import org.apache.skywalking.oap.server.core.source.LogMetadata;
 import org.apache.skywalking.oap.server.library.module.ModuleManager;
 import org.apache.skywalking.oap.server.library.module.ModuleStartException;
 import org.apache.skywalking.oap.server.receiver.envoy.EnvoyMetricReceiverConfig;
@@ -66,8 +65,16 @@ public class LogsPersistence implements ALSHTTPAnalysis {
                 return result;
             }
 
-            final LogData logData = convertToLogData(entry, result);
-            logAnalyzerService.doAnalysis(logData, Optional.of(entry));
+            final ServiceMetaInfo service = result.getService();
+            final HTTPServiceMeshMetric.Builder metrics =
+                new LogEntry2MetricsAdapter(entry, null, null).adaptCommonPart();
+            final LogMetadata metadata = LogMetadata.builder()
+                .service(service.getServiceName())
+                .serviceInstance(service.getServiceInstanceName())
+                .timestamp(metrics.getEndTime())
+                .layer(Layer.MESH.name())
+                .build();
+            logAnalyzerService.doAnalysis(metadata, entry);
         } catch (final Exception e) {
             log.error("Failed to persist Envoy access log", e);
         }
@@ -78,19 +85,4 @@ public class LogsPersistence implements ALSHTTPAnalysis {
     public Role identify(final StreamAccessLogsMessage.Identifier alsIdentifier, final Role prev) {
         return prev;
     }
-
-    public LogData convertToLogData(final HTTPAccessLogEntry logEntry, final Result result) {
-        final ServiceMetaInfo service = result.getService();
-
-        final HTTPServiceMeshMetric.Builder metrics =
-            new LogEntry2MetricsAdapter(logEntry, null, null).adaptCommonPart();
-
-        return LogData
-            .newBuilder()
-            .setService(service.getServiceName())
-            .setServiceInstance(service.getServiceInstanceName())
-            .setTimestamp(metrics.getEndTime())
-            .setLayer(Layer.MESH.name())
-            .build();
-    }
 }
diff --git a/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/TCPLogsPersistence.java b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/TCPLogsPersistence.java
index ac66002d1f..be942164d8 100644
--- a/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/TCPLogsPersistence.java
+++ b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/TCPLogsPersistence.java
@@ -18,12 +18,11 @@
 
 package org.apache.skywalking.oap.server.receiver.envoy.persistence;
 
-import java.util.Optional;
-import org.apache.skywalking.apm.network.logging.v3.LogData;
 import org.apache.skywalking.apm.network.servicemesh.v3.TCPServiceMeshMetric;
 import org.apache.skywalking.oap.log.analyzer.v2.module.LogAnalyzerModule;
 import org.apache.skywalking.oap.log.analyzer.v2.provider.log.ILogAnalyzerService;
 import org.apache.skywalking.oap.server.core.analysis.Layer;
+import org.apache.skywalking.oap.server.core.source.LogMetadata;
 import org.apache.skywalking.oap.server.library.module.ModuleManager;
 import org.apache.skywalking.oap.server.library.module.ModuleStartException;
 import org.apache.skywalking.oap.server.receiver.envoy.EnvoyMetricReceiverConfig;
@@ -66,8 +65,16 @@ public class TCPLogsPersistence implements TCPAccessLogAnalyzer {
                 return result;
             }
 
-            final LogData logData = convertToLogData(entry, result);
-            logAnalyzerService.doAnalysis(logData, Optional.of(entry));
+            final ServiceMetaInfo service = result.getService();
+            final TCPServiceMeshMetric.Builder metrics =
+                new TCPLogEntry2MetricsAdapter(entry, null, null).adaptCommonPart();
+            final LogMetadata metadata = LogMetadata.builder()
+                .service(service.getServiceName())
+                .serviceInstance(service.getServiceInstanceName())
+                .timestamp(metrics.getEndTime())
+                .layer(Layer.MESH.name())
+                .build();
+            logAnalyzerService.doAnalysis(metadata, entry);
         } catch (final Exception e) {
             log.error("Failed to persist Envoy access log", e);
         }
@@ -78,19 +85,4 @@ public class TCPLogsPersistence implements TCPAccessLogAnalyzer {
     public Role identify(final StreamAccessLogsMessage.Identifier alsIdentifier, final Role prev) {
         return prev;
     }
-
-    public LogData convertToLogData(final TCPAccessLogEntry logEntry, final Result result) {
-        final ServiceMetaInfo service = result.getService();
-
-        final TCPServiceMeshMetric.Builder metrics =
-            new TCPLogEntry2MetricsAdapter(logEntry, null, null).adaptCommonPart();
-
-        return LogData
-            .newBuilder()
-            .setService(service.getServiceName())
-            .setServiceInstance(service.getServiceInstanceName())
-            .setTimestamp(metrics.getEndTime())
-            .setLayer(Layer.MESH.name())
-            .build();
-    }
 }
diff --git a/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/test/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/EnvoyAccessLogBuilderTest.java b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/test/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/EnvoyAccessLogBuilderTest.java
index 7bc3b0f89d..2c3b39536f 100644
--- a/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/test/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/EnvoyAccessLogBuilderTest.java
+++ b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/test/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/EnvoyAccessLogBuilderTest.java
@@ -23,8 +23,7 @@ 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.util.Collections;
-import java.util.Optional;
-import org.apache.skywalking.apm.network.logging.v3.LogData;
+import org.apache.skywalking.oap.server.core.source.LogMetadata;
 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;
@@ -87,11 +86,11 @@ class EnvoyAccessLogBuilderTest {
         builder.setService("test-svc");
         builder.setTimestamp(1609459200000L);
 
-        final LogData logData = LogData.newBuilder()
-            .setService("test-svc")
-            .setTimestamp(1609459200000L)
+        final LogMetadata metadata = LogMetadata.builder()
+            .service("test-svc")
+            .timestamp(1609459200000L)
             .build();
-        builder.init(logData, Optional.of(entry), moduleManager);
+        builder.init(metadata, entry, moduleManager);
 
         final Log log = builder.toLog();
 
@@ -109,12 +108,12 @@ class EnvoyAccessLogBuilderTest {
         builder.setService("test-svc");
         builder.setTimestamp(1609459200000L);
 
-        final LogData logData = LogData.newBuilder()
-            .setService("test-svc")
-            .setTimestamp(1609459200000L)
+        final LogMetadata metadata = LogMetadata.builder()
+            .service("test-svc")
+            .timestamp(1609459200000L)
             .build();
         // Pass a default (empty) entry — no response code, so toLog() serializes empty JSON
-        builder.init(logData, Optional.of(HTTPAccessLogEntry.getDefaultInstance()), moduleManager);
+        builder.init(metadata, HTTPAccessLogEntry.getDefaultInstance(), moduleManager);
 
         final Log log = builder.toLog();
 
@@ -159,12 +158,13 @@ class EnvoyAccessLogBuilderTest {
 
         final LalExpression expr = generator.compile(dsl);
 
-        // Build LogData with service/instance (simulates what LogsPersistence creates)
-        final LogData.Builder logData = LogData.newBuilder()
-            .setService("envoy-test-svc")
-            .setServiceInstance("envoy-test-instance")
-            .setTimestamp(1609459200000L)
-            .setLayer("MESH");
+        // Build LogMetadata (simulates what LogsPersistence creates)
+        final LogMetadata metadata = LogMetadata.builder()
+            .service("envoy-test-svc")
+            .serviceInstance("envoy-test-instance")
+            .timestamp(1609459200000L)
+            .layer("MESH")
+            .build();
 
         // Build HTTPAccessLogEntry with response code 503
         final HTTPAccessLogEntry entry = HTTPAccessLogEntry.newBuilder()
@@ -177,8 +177,7 @@ class EnvoyAccessLogBuilderTest {
         // Execute
         final FilterSpec filterSpec = buildFilterSpec();
         final ExecutionContext ctx = new ExecutionContext();
-        ctx.log(logData);
-        ctx.extraLog(entry);
+        ctx.init(metadata, entry);
         expr.execute(filterSpec, ctx);
 
         // Verify output is EnvoyAccessLogBuilder
@@ -196,7 +195,7 @@ class EnvoyAccessLogBuilderTest {
         when(cs.getSearchableLogsTags()).thenReturn("status.code,svc");
         // Reset static initialized flag so the new config takes effect
         resetLogBuilderState();
-        output.init(logData.build(), Optional.of(entry), testMm);
+        output.init(metadata, entry, testMm);
         final Log log = output.toLog();
 
         assertTrue(log.getTags().stream().anyMatch(
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
index 9a9cb6a96e..050fdae597 100644
--- 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
@@ -26,9 +26,8 @@ import io.envoyproxy.envoy.data.accesslog.v3.HTTPAccessLogEntry;
 import io.envoyproxy.envoy.data.accesslog.v3.HTTPResponseProperties;
 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.server.core.source.LogMetadata;
 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;
@@ -306,15 +305,15 @@ class EnvoyAlsLalTest {
     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 LogMetadata metadata = LogMetadata.builder()
+            .service("als-test-svc")
+            .timestamp(1609459200000L)
+            .layer("MESH")
+            .build();
 
         final FilterSpec filterSpec = buildFilterSpec();
         final ExecutionContext ctx = new ExecutionContext();
-        ctx.log(logData);
-        ctx.extraLog(entry);
+        ctx.init(metadata, entry);
         expr.execute(filterSpec, ctx);
         return ctx;
     }
@@ -332,7 +331,7 @@ class EnvoyAlsLalTest {
         resetLogBuilderState();
         final ModuleManager tagMm = buildCoreModuleManager(
             String.join(",", searchableTagKeys));
-        output.init(ctx.log().build(), Optional.of(entry), tagMm);
+        output.init(ctx.metadata(), entry, tagMm);
         return output.toLog();
     }
 
diff --git a/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryLogHandler.java b/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryLogHandler.java
index b2b526eef8..9bebac1077 100644
--- a/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryLogHandler.java
+++ b/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryLogHandler.java
@@ -25,7 +25,6 @@ import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse;
 import io.opentelemetry.proto.collector.logs.v1.LogsServiceGrpc;
 import io.opentelemetry.proto.common.v1.KeyValue;
 import io.opentelemetry.proto.logs.v1.LogRecord;
-import java.util.Optional;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import lombok.Getter;
@@ -34,6 +33,7 @@ import org.apache.skywalking.apm.network.logging.v3.LogData;
 import org.apache.skywalking.apm.network.logging.v3.LogDataBody;
 import org.apache.skywalking.apm.network.logging.v3.LogTags;
 import org.apache.skywalking.apm.network.logging.v3.TextLog;
+import org.apache.skywalking.oap.server.core.source.LogMetadataUtils;
 import org.apache.skywalking.oap.log.analyzer.v2.module.LogAnalyzerModule;
 import org.apache.skywalking.oap.log.analyzer.v2.provider.log.ILogAnalyzerService;
 import org.apache.skywalking.oap.server.core.server.GRPCHandlerRegister;
@@ -122,16 +122,15 @@ public class OpenTelemetryLogHandler
 
     private void doAnalysisQuietly(String service, String layer, String serviceInstance, LogRecord logRecord) {
         try {
-            logAnalyzerService().doAnalysis(
-                LogData
-                    .newBuilder()
-                    .setService(service)
-                    .setServiceInstance(serviceInstance)
-                    .setTimestamp(logRecord.getTimeUnixNano() / 1_000_000)
-                    .setTags(buildTags(logRecord))
-                    .setBody(buildBody(logRecord))
-                    .setLayer(layer),
-                Optional.empty());
+            final LogData.Builder builder = LogData
+                .newBuilder()
+                .setService(service)
+                .setServiceInstance(serviceInstance)
+                .setTimestamp(logRecord.getTimeUnixNano() / 1_000_000)
+                .setTags(buildTags(logRecord))
+                .setBody(buildBody(logRecord))
+                .setLayer(layer);
+            logAnalyzerService().doAnalysis(LogMetadataUtils.fromLogData(builder), builder);
         } catch (Exception e) {
             log.error("Failed to analyze logs", e);
         }
diff --git a/oap-server/server-receiver-plugin/skywalking-log-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/log/provider/handler/grpc/LogReportServiceGrpcHandler.java b/oap-server/server-receiver-plugin/skywalking-log-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/log/provider/handler/grpc/LogReportServiceGrpcHandler.java
index d243142b60..4d7563fd09 100644
--- a/oap-server/server-receiver-plugin/skywalking-log-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/log/provider/handler/grpc/LogReportServiceGrpcHandler.java
+++ b/oap-server/server-receiver-plugin/skywalking-log-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/log/provider/handler/grpc/LogReportServiceGrpcHandler.java
@@ -18,11 +18,11 @@
 package org.apache.skywalking.oap.server.receiver.log.provider.handler.grpc;
 
 import io.grpc.stub.StreamObserver;
-import java.util.Optional;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.skywalking.apm.network.common.v3.Commands;
 import org.apache.skywalking.apm.network.logging.v3.LogData;
 import org.apache.skywalking.apm.network.logging.v3.LogReportServiceGrpc;
+import org.apache.skywalking.oap.server.core.source.LogMetadataUtils;
 import org.apache.skywalking.oap.server.library.util.StringUtil;
 import org.apache.skywalking.oap.log.analyzer.v2.module.LogAnalyzerModule;
 import org.apache.skywalking.oap.log.analyzer.v2.provider.log.ILogAnalyzerService;
@@ -93,7 +93,7 @@ public class LogReportServiceGrpcHandler extends LogReportServiceGrpc.LogReportS
                 try {
                     LogData.Builder builder = logData.toBuilder();
                     setServiceName(builder);
-                    logAnalyzerService.doAnalysis(builder, Optional.empty());
+                    logAnalyzerService.doAnalysis(LogMetadataUtils.fromLogData(builder), builder);
                 } catch (Exception e) {
                     errorCounter.inc();
                     log.error(e.getMessage(), e);
diff --git a/oap-server/server-receiver-plugin/skywalking-log-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/log/provider/handler/rest/LogReportServiceHTTPHandler.java b/oap-server/server-receiver-plugin/skywalking-log-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/log/provider/handler/rest/LogReportServiceHTTPHandler.java
index 32a3a761fc..bcb64282ec 100644
--- a/oap-server/server-receiver-plugin/skywalking-log-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/log/provider/handler/rest/LogReportServiceHTTPHandler.java
+++ b/oap-server/server-receiver-plugin/skywalking-log-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/log/provider/handler/rest/LogReportServiceHTTPHandler.java
@@ -19,10 +19,10 @@ package org.apache.skywalking.oap.server.receiver.log.provider.handler.rest;
 
 import com.linecorp.armeria.server.annotation.Post;
 import java.util.List;
-import java.util.Optional;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.skywalking.apm.network.common.v3.Commands;
 import org.apache.skywalking.apm.network.logging.v3.LogData;
+import org.apache.skywalking.oap.server.core.source.LogMetadataUtils;
 import org.apache.skywalking.oap.log.analyzer.v2.module.LogAnalyzerModule;
 import org.apache.skywalking.oap.log.analyzer.v2.provider.log.ILogAnalyzerService;
 import org.apache.skywalking.oap.server.library.module.ModuleManager;
@@ -64,7 +64,10 @@ public class LogReportServiceHTTPHandler {
     @Post("/v3/logs")
     public Commands collectLogs(final List<LogData> logs) {
         try (final HistogramMetrics.Timer ignored = histogram.createTimer()) {
-            logs.forEach(it -> logAnalyzerService.doAnalysis(it, Optional.empty()));
+            logs.forEach(it -> {
+                final LogData.Builder builder = it.toBuilder();
+                logAnalyzerService.doAnalysis(LogMetadataUtils.fromLogData(builder), builder);
+            });
             return Commands.newBuilder().build();
         } catch (final Throwable e) {
             errorCounter.inc();
diff --git a/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalBenchmark.java b/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalBenchmark.java
index b8178394b2..1ff1b92e79 100644
--- a/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalBenchmark.java
+++ b/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalBenchmark.java
@@ -239,12 +239,14 @@ public class LalBenchmark {
     public void executeV2(final Blackhole bh) {
         for (int i = 0; i < v2Exprs.size(); i++) {
             try {
+                final LogData logData = testLogs.get(i);
+                final Message extraLog = extraLogs.get(i);
+                final org.apache.skywalking.oap.server.core.source.LogMetadata metadata =
+                    org.apache.skywalking.oap.server.core.source.LogMetadataUtils.fromLogData(logData);
+                final Object input = extraLog != null ? extraLog : logData.toBuilder();
                 final org.apache.skywalking.oap.log.analyzer.v2.dsl.ExecutionContext ctx =
-                    new org.apache.skywalking.oap.log.analyzer.v2.dsl.ExecutionContext()
-                        .log(testLogs.get(i));
-                if (extraLogs.get(i) != null) {
-                    ctx.extraLog(extraLogs.get(i));
-                }
+                    new org.apache.skywalking.oap.log.analyzer.v2.dsl.ExecutionContext();
+                ctx.init(metadata, input);
                 v2Exprs.get(i).execute(v2FilterSpec, ctx);
                 bh.consume(ctx);
             } catch (Exception ignored) {
diff --git a/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalComparisonTest.java b/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalComparisonTest.java
index 70b6a104d4..437dfdd54b 100644
--- a/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalComparisonTest.java
+++ b/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/lal/LalComparisonTest.java
@@ -165,12 +165,13 @@ class LalComparisonTest {
             final String v2CompileError,
             final boolean v2Only) throws Exception {
 
-        final LogData testLog;
+        final LogData.Builder testLogBuilder;
         if (inputData != null) {
-            testLog = LalLogDataBuilder.buildLogData(inputData).build();
+            testLogBuilder = LalLogDataBuilder.buildLogData(inputData);
         } else {
-            testLog = LalLogDataBuilder.buildSyntheticLogData(dsl);
+            testLogBuilder = LalLogDataBuilder.buildSyntheticLogData(dsl).toBuilder();
         }
+        final LogData testLog = testLogBuilder.build();
 
         // Build proto extraLog from input data if available
         final Message extraLog = inputData != null
@@ -215,10 +216,11 @@ class LalComparisonTest {
                         v2Manager, new LogAnalyzerModuleConfig());
                 disableSinkListenersOnSpec(v2FilterSpec);
 
-                v2Ctx = new org.apache.skywalking.oap.log.analyzer.v2.dsl.ExecutionContext().log(testLog);
-                if (extraLog != null) {
-                    v2Ctx.extraLog(extraLog);
-                }
+                final org.apache.skywalking.oap.server.core.source.LogMetadata v2Metadata =
+                    org.apache.skywalking.oap.server.core.source.LogMetadataUtils.fromLogData(testLog);
+                final Object v2Input = extraLog != null ? extraLog : testLogBuilder;
+                v2Ctx = new org.apache.skywalking.oap.log.analyzer.v2.dsl.ExecutionContext();
+                v2Ctx.init(v2Metadata, v2Input);
 
                 v2Expr.execute(v2FilterSpec, v2Ctx);
             } catch (Exception e) {
@@ -247,25 +249,26 @@ class LalComparisonTest {
             // If the extractor sets a field, v1 puts it on LogData, v2 puts it on LogBuilder.
             // If the extractor doesn't set a field, both keep the original LogData value.
             final Object v2Output = v2Ctx.output();
+            final org.apache.skywalking.oap.server.core.source.LogMetadata v2Meta = v2Ctx.metadata();
             if (v2Output instanceof org.apache.skywalking.oap.server.core.source.LogBuilder) {
                 final org.apache.skywalking.oap.server.core.source.LogBuilder v2Builder =
                     (org.apache.skywalking.oap.server.core.source.LogBuilder) v2Output;
                 assertV2Field(testName, "service", v1Log.getService(),
-                    getFieldValue(v2Builder, "service", null), v2Ctx.log().getService());
+                    getFieldValue(v2Builder, "service", null), v2Meta.getService());
                 assertV2Field(testName, "serviceInstance", v1Log.getServiceInstance(),
                     getFieldValue(v2Builder, "serviceInstance", null),
-                    v2Ctx.log().getServiceInstance());
+                    v2Meta.getServiceInstance());
                 assertV2Field(testName, "endpoint", v1Log.getEndpoint(),
-                    getFieldValue(v2Builder, "endpoint", null), v2Ctx.log().getEndpoint());
+                    getFieldValue(v2Builder, "endpoint", null), v2Meta.getEndpoint());
                 assertV2Field(testName, "layer", v1Log.getLayer(),
-                    getFieldValue(v2Builder, "layer", null), v2Ctx.log().getLayer());
+                    getFieldValue(v2Builder, "layer", null), v2Meta.getLayer());
                 final long v2Ts = v2Builder.getTimestamp();
-                assertEquals(v1Log.getTimestamp(), v2Ts != 0 ? v2Ts : v2Ctx.log().getTimestamp(),
+                assertEquals(v1Log.getTimestamp(), v2Ts != 0 ? v2Ts : v2Meta.getTimestamp(),
                     testName + ": timestamp mismatch");
                 // Compare tags: v1 stores in LogData.tags, v2 stores in LogBuilder.lalTags
                 compareV1TagsToV2Builder(testName, v1Log, v2Builder);
-            } else {
-                final LogData.Builder v2Log = v2Ctx.log();
+            } else if (v2Ctx.input() instanceof LogData.Builder) {
+                final LogData.Builder v2Log = (LogData.Builder) v2Ctx.input();
                 assertEquals(v1Log.getService(), v2Log.getService(),
                     testName + ": service mismatch");
                 assertEquals(v1Log.getServiceInstance(), v2Log.getServiceInstance(),
@@ -278,6 +281,16 @@ class LalComparisonTest {
                     testName + ": timestamp mismatch");
                 assertEquals(v1Log.getTags(), v2Log.getTags(),
                     testName + ": tags mismatch");
+            } else {
+                // ALS case: no LogData, compare metadata
+                assertEquals(v1Log.getService(), v2Meta.getService(),
+                    testName + ": service mismatch");
+                assertEquals(v1Log.getServiceInstance(), v2Meta.getServiceInstance(),
+                    testName + ": serviceInstance mismatch");
+                assertEquals(v1Log.getLayer(), v2Meta.getLayer(),
+                    testName + ": layer mismatch");
+                assertEquals(v1Log.getTimestamp(), v2Meta.getTimestamp(),
+                    testName + ": timestamp mismatch");
             }
         }
 
@@ -287,7 +300,8 @@ class LalComparisonTest {
             final Map<String, Object> expect =
                 (Map<String, Object>) inputData.get("expect");
             if (expect != null) {
-                final LogData.Builder v2Log = v2Ctx.log();
+                final LogData.Builder v2Log = v2Ctx.input() instanceof LogData.Builder
+                    ? (LogData.Builder) v2Ctx.input() : null;
                 validateExpected(testName, v2Ctx, v2Log, expect);
             }
         }