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/07/03 04:11:31 UTC

(skywalking) branch feat/lal-drop-reason created (now c9927efcdf)

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

wu-sheng pushed a change to branch feat/lal-drop-reason
in repository https://gitbox.apache.org/repos/asf/skywalking.git


      at c9927efcdf Surface LAL drop reason in DSL live-debugging

This branch includes the following new commits:

     new c9927efcdf Surface LAL drop reason in DSL live-debugging

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(skywalking) 01/01: Surface LAL drop reason in DSL live-debugging

Posted by wu...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

wu-sheng pushed a commit to branch feat/lal-drop-reason
in repository https://gitbox.apache.org/repos/asf/skywalking.git

commit c9927efcdf11a4a52cfd55cb6096e159253955e9
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Fri Jul 3 12:11:16 2026 +0800

    Surface LAL drop reason in DSL live-debugging
    
    When a LAL rule stops a log at a parse step, live-debug could only show
    continueOn=false (that it stopped, never why). Add a human-readable reason:
    
    - ExecutionContext.dropReason(String)/dropReason(): set at the LAL drop sites
      (json/yaml parse failure, text regexp non-match, non-log-body input) in
      FilterSpec/TextParserSpec.
    - The shared debug Sample gains an optional reason field (non-final, so the
      5-arg constructor and MAL/OAL recorders are unchanged); the LAL recorder
      stamps it from ctx.dropReason() only when the step stopped (continueOn=false),
      so the reason lands on the parser step that halted the pipeline.
    - Reason flows through the dsl-debugging REST session response and the cluster
      forward proto (ClusterSample.reason) for single-node and cross-node sessions.
    - Document per-DSL TYPE_* usage inline on Sample.
    
    Co-Authored-By: Claude Fable 5 <no...@anthropic.com>
---
 docs/en/changes/changes.md                         |  1 +
 .../oap/log/analyzer/v2/dsl/ExecutionContext.java  | 18 +++++++++++++++
 .../analyzer/v2/dsl/spec/filter/FilterSpec.java    | 20 +++++++++++-----
 .../v2/dsl/spec/parser/TextParserSpec.java         |  1 +
 .../v2/compiler/LALScriptExecutionTest.java        |  8 +++++++
 .../feature-cases/execution-basic.data.yaml        | 11 ++++++---
 .../cluster/DSLDebuggingClusterServiceImpl.java    |  1 +
 .../dsl/debugging/lal/LALDebugRecorderImpl.java    | 27 ++++++++++++++++------
 .../debugging/rest/DSLDebuggingRestHandler.java    |  6 +++++
 .../server/admin/dsl/debugging/session/Sample.java | 24 ++++++++++++++++++-
 .../src/main/proto/dsl-debugging-cluster.proto     |  4 ++++
 11 files changed, 104 insertions(+), 17 deletions(-)

diff --git a/docs/en/changes/changes.md b/docs/en/changes/changes.md
index 2bf842f8db..950d00b557 100644
--- a/docs/en/changes/changes.md
+++ b/docs/en/changes/changes.md
@@ -265,6 +265,7 @@
 * Push runtime-rule convergence to peers on commit. After a successful structural apply — and on the `commit_deferred` path, where the DB row is durable but this node's commit-tail threw — the main broadcasts a `NotifyApplied` admin-internal RPC so peers reconcile against the just-persisted DB row immediately, instead of waiting up to one refresh tick (~30s) to notice it. The fan-out runs off the REST response thread (fire-and-forget on a daemon executor) so an unreachable peer's per-cal [...]
 * Fix BanyanDB peer nodes permanently flooding `<metric> is not registered`, and a follow-on case where a peer kept translating writes with a stale schema shape after a runtime-rule reshape, when a node held a live persist worker but its local `MetadataRegistry` schema cache was missing or stale for that model — a `withoutSchemaChange` peer apply or a runtime-rule bundled fall-over rebuilt the dispatch worker but skipped the local-cache populate, and the registry was insert-only (never e [...]
 * Support LAL `json {}` parsing JSON content delivered in a plain-text log body. The parser reads the native protocol's JSON body first; when that is empty, it tries the text body as JSON — e.g. the OTLP log receiver maps every OTLP string body to a text body, even JSON-shaped ones, so previously-aborting `json {}` rules on OTLP-fed layers now work without any receiver or protocol change. On a successful parse from a text body, the matching rule persists the log as a JSON body with conte [...]
+* Surface the drop reason in LAL live-debugging. When a LAL rule stops a log at a parse step (a `json {}` / `yaml {}` parse failure, a `text { regexp }` non-match, or a non-log-body input), the recorder now captures a human-readable `reason` (e.g. the parse exception) onto the DSL-debug `Sample`, exposed through the `dsl-debugging` REST session response and the cluster forward proto. Previously a live-debug watcher could only see `continueOn=false` — that a step stopped, never why — and  [...]
 * Fix a v2 MAL `CounterWindow` key collision: `rate()` / `increase()` / `irate()` keyed each counter's sliding window on the rule's output metric name (the same for every input metric of a rule) instead of the counter's own name, so two or more counters that reduce to the same label set after `.sum(...)` shared one window and computed rates against each other's values — fabricating non-zero rates from unchanged counters (e.g. the BanyanDB liaison gRPC error rate read a steady non-zero of [...]
 * Fix the v2 MAL Elvis operator `?:` to honor Groovy-falsy semantics. It compiled to `Optional.ofNullable(primary).orElse(fallback)`, applying the fallback only when the primary is `null`, so an empty-string primary kept `""` instead — e.g. a BanyanDB liaison `ServiceInstance` stored `node_type=""` rather than `n/a`, because `.sum([...,'node_type'])` fills an absent group-by label with `""`. The fallback now applies for falsy primaries such as null, false, numeric zero, and empty strings [...]
 * SWIP-15: rebuild BanyanDB self-observability around the cluster / container / group model (requires BanyanDB 0.11+). A BanyanDB cluster is modeled as one `Service`, each container as a `ServiceInstance` (role/tier as attributes), and each storage group as an `Endpoint`. The `otel-rules/banyandb/` rules are category-separated by role (`node_*` / `liaison_*` / `data_*` / `lifecycle_*`) and by data type (`measure_*` / `stream_*` / `trace_*` / `property_*`), mirroring the upstream FODC-pro [...]
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 8e98c5b6a0..44c7985248 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
@@ -57,6 +57,7 @@ public class ExecutionContext {
     public static final String KEY_DRY_RUN = "dry_run";
     public static final String KEY_AUTO_LAYER = "auto_layer";
     public static final String KEY_OUTPUT = "output";
+    public static final String KEY_DROP_REASON = "drop_reason";
 
     private final Map<String, Object> properties = new HashMap<>();
 
@@ -112,6 +113,7 @@ public class ExecutionContext {
         setProperty(KEY_METRICS_CONTAINER, null);
         setProperty(KEY_DRY_RUN, false);
         setProperty(KEY_OUTPUT, null);
+        setProperty(KEY_DROP_REASON, null);
         return this;
     }
 
@@ -174,6 +176,22 @@ public class ExecutionContext {
         return (boolean) getProperty(KEY_ABORT);
     }
 
+    /**
+     * Record a human-readable reason the pipeline stopped/dropped at the current step
+     * (e.g. a parse-failure message). Read by the DSL-debug recorder to populate
+     * {@code Sample.reason} so a live-debug watcher sees WHY a log was dropped, not just
+     * that it was. Independent of {@link #abort()}: callers set the reason then abort (or,
+     * on the continue-on-failure path, set it without aborting).
+     */
+    public ExecutionContext dropReason(final String reason) {
+        setProperty(KEY_DROP_REASON, reason);
+        return this;
+    }
+
+    public String dropReason() {
+        return (String) getProperty(KEY_DROP_REASON);
+    }
+
     public ExecutionContext metricsContainer(final List<SampleFamily> container) {
         setProperty(KEY_METRICS_CONTAINER, container);
         return this;
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 e1b95c719f..34c51a3133 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
@@ -157,7 +157,7 @@ public class FilterSpec extends AbstractSpec {
         }
         final Object rawInput = ctx.input();
         if (!(rawInput instanceof LogData.Builder)) {
-            abortOrContinueUnparsed(ctx, abortOnFailure);
+            abortOrContinueUnparsed(ctx, abortOnFailure, notLogBodyReason("json", rawInput));
             return;
         }
         try {
@@ -178,7 +178,7 @@ public class FilterSpec extends AbstractSpec {
             }
         } catch (final Exception e) {
             warnParseFailure(jsonWarnLimiter, "json", ctx, abortOnFailure, e);
-            abortOrContinueUnparsed(ctx, abortOnFailure);
+            abortOrContinueUnparsed(ctx, abortOnFailure, "json parse failed: " + e);
         }
     }
 
@@ -194,7 +194,7 @@ public class FilterSpec extends AbstractSpec {
         }
         final Object rawInput = ctx.input();
         if (!(rawInput instanceof LogData.Builder)) {
-            abortOrContinueUnparsed(ctx, abortOnFailure);
+            abortOrContinueUnparsed(ctx, abortOnFailure, notLogBodyReason("yaml", rawInput));
             return;
         }
         try {
@@ -206,17 +206,20 @@ public class FilterSpec extends AbstractSpec {
             ctx.parsed(parsed);
         } catch (final Exception e) {
             warnParseFailure(yamlWarnLimiter, "yaml", ctx, abortOnFailure, e);
-            abortOrContinueUnparsed(ctx, abortOnFailure);
+            abortOrContinueUnparsed(ctx, abortOnFailure, "yaml parse failed: " + e);
         }
     }
 
     /**
-     * Failed-parse epilogue: abort when the rule demands it; otherwise install a
+     * Failed-parse epilogue: record the drop reason (surfaced in live-debug via
+     * {@code Sample.reason}), then abort when the rule demands it; otherwise install a
      * metadata-only parsed map so downstream {@code parsed.*} reads stay null-safe on
      * the continuation path — without it the generated extractor would NPE on the null
      * map and drop the log despite {@code abortOnFailure false}.
      */
-    private void abortOrContinueUnparsed(final ExecutionContext ctx, final boolean abortOnFailure) {
+    private void abortOrContinueUnparsed(final ExecutionContext ctx, final boolean abortOnFailure,
+                                         final String reason) {
+        ctx.dropReason(reason);
         if (abortOnFailure) {
             ctx.abort();
             return;
@@ -226,6 +229,11 @@ public class FilterSpec extends AbstractSpec {
         ctx.parsed(parsed);
     }
 
+    private static String notLogBodyReason(final String parser, final Object rawInput) {
+        final String actual = rawInput == null ? "null" : rawInput.getClass().getSimpleName();
+        return parser + " parser: input is not a text/JSON log body (" + actual + ")";
+    }
+
     /**
      * Parse-failure logging shared by {@code json {}} / {@code yaml {}}: WARN when the
      * failure aborts the log (rate-limited, with the suppressed count reported on the
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 e8ee67037c..3ac0cbbbbf 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
@@ -50,6 +50,7 @@ public class TextParserSpec extends AbstractParserSpec {
         if (matched) {
             ctx.parsed(matcher);
         } else {
+            ctx.dropReason("text parser: regexp did not match the log body");
             if (abortOnFailure) {
                 final long suppressed = warnLimiter.acquire();
                 if (suppressed >= 0) {
diff --git a/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALScriptExecutionTest.java b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALScriptExecutionTest.java
index 1cdca79c46..ef349ad002 100644
--- a/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALScriptExecutionTest.java
+++ b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALScriptExecutionTest.java
@@ -52,6 +52,7 @@ import org.junit.jupiter.api.TestFactory;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.mock;
@@ -254,6 +255,13 @@ class LALScriptExecutionTest {
                     assertEquals(expected, bodyContentType(logBuilder),
                         ruleName + ": persisted body content type mismatch");
                     break;
+                case "dropReason": {
+                    final String reason = ctx.dropReason();
+                    assertNotNull(reason, ruleName + ": expected a drop reason on ctx");
+                    assertTrue(reason.contains(expected),
+                        ruleName + ": drop reason '" + reason + "' should contain '" + expected + "'");
+                    break;
+                }
                 default:
                     if (key.startsWith("tag.")) {
                         final String tagKey = key.substring(4);
diff --git a/oap-server/analyzer/log-analyzer/src/test/resources/scripts/lal/test-lal/feature-cases/execution-basic.data.yaml b/oap-server/analyzer/log-analyzer/src/test/resources/scripts/lal/test-lal/feature-cases/execution-basic.data.yaml
index 066dd39033..243c7a4140 100644
--- a/oap-server/analyzer/log-analyzer/src/test/resources/scripts/lal/test-lal/feature-cases/execution-basic.data.yaml
+++ b/oap-server/analyzer/log-analyzer/src/test/resources/scripts/lal/test-lal/feature-cases/execution-basic.data.yaml
@@ -213,15 +213,18 @@ json-text-fallback:
     tag.env: prod
     contentType: JSON
 
-# Non-JSON text under the fallback still aborts (default abortOnFailure true).
+# Non-JSON text under the fallback still aborts (default abortOnFailure true), and the
+# drop reason is recorded on ctx so live-debug can surface WHY it dropped.
 json-text-fallback-invalid:
   body-type: text
   body: 'plain non-json log line'
   expect:
     abort: true
+    dropReason: "json parse failed"
 
 # abortOnFailure false: a json parse failure must NOT abort — the log flows through,
-# and parsed.* reads stay null-safe with metadata values as fallback.
+# and parsed.* reads stay null-safe with metadata values as fallback. The reason is
+# still recorded even though the pipeline continued.
 json-abort-on-failure-false:
   service: fallback-svc
   body-type: text
@@ -230,11 +233,13 @@ json-abort-on-failure-false:
     abort: false
     save: true
     service: fallback-svc
+    dropReason: "json parse failed"
 
-# abortOnFailure false on a text regexp: a no-match must NOT abort.
+# abortOnFailure false on a text regexp: a no-match must NOT abort, but records the reason.
 text-abort-on-failure-false:
   body-type: text
   body: 'a line that does not match the level pattern'
   expect:
     abort: false
     save: true
+    dropReason: "regexp did not match"
diff --git a/oap-server/server-admin/dsl-debugging/src/main/java/org/apache/skywalking/oap/server/admin/dsl/debugging/cluster/DSLDebuggingClusterServiceImpl.java b/oap-server/server-admin/dsl-debugging/src/main/java/org/apache/skywalking/oap/server/admin/dsl/debugging/cluster/DSLDebuggingClusterServiceImpl.java
index f1badcd6e8..b6d18ff9fd 100644
--- a/oap-server/server-admin/dsl-debugging/src/main/java/org/apache/skywalking/oap/server/admin/dsl/debugging/cluster/DSLDebuggingClusterServiceImpl.java
+++ b/oap-server/server-admin/dsl-debugging/src/main/java/org/apache/skywalking/oap/server/admin/dsl/debugging/cluster/DSLDebuggingClusterServiceImpl.java
@@ -195,6 +195,7 @@ public final class DSLDebuggingClusterServiceImpl
             .setContinueOn(s.isContinueOn())
             .setPayloadJson(s.getPayloadJson() == null ? "{}" : s.getPayloadJson())
             .setSourceLine(s.getSourceLine())
+            .setReason(s.getReason() == null ? "" : s.getReason())
             .build();
     }
 }
diff --git a/oap-server/server-admin/dsl-debugging/src/main/java/org/apache/skywalking/oap/server/admin/dsl/debugging/lal/LALDebugRecorderImpl.java b/oap-server/server-admin/dsl-debugging/src/main/java/org/apache/skywalking/oap/server/admin/dsl/debugging/lal/LALDebugRecorderImpl.java
index 202bfee8bd..071ef3c195 100644
--- a/oap-server/server-admin/dsl-debugging/src/main/java/org/apache/skywalking/oap/server/admin/dsl/debugging/lal/LALDebugRecorderImpl.java
+++ b/oap-server/server-admin/dsl-debugging/src/main/java/org/apache/skywalking/oap/server/admin/dsl/debugging/lal/LALDebugRecorderImpl.java
@@ -72,12 +72,12 @@ public final class LALDebugRecorderImpl extends AbstractDebugRecorder
         // subsequent samples render only the output (builder state) so the
         // wire doesn't repeat the raw input on every probe.
         publishCurrentExecution();
-        addSample(new Sample(Sample.TYPE_INPUT, "", continueOn(ctx), inputPayload(ctx), sourceLine));
+        addSample(sample(Sample.TYPE_INPUT, "", ctx, inputPayload(ctx), sourceLine));
     }
 
     @Override
     public void appendParser(final String rule, final int sourceLine, final ExecutionContext ctx) {
-        addSample(new Sample(Sample.TYPE_FUNCTION, "", continueOn(ctx), outputPayload(ctx), sourceLine));
+        addSample(sample(Sample.TYPE_FUNCTION, "", ctx, outputPayload(ctx), sourceLine));
     }
 
     @Override
@@ -87,7 +87,7 @@ public final class LALDebugRecorderImpl extends AbstractDebugRecorder
             // block-level synopsis to avoid noise.
             return;
         }
-        addSample(new Sample(Sample.TYPE_FUNCTION, "", continueOn(ctx), outputPayload(ctx), sourceLine));
+        addSample(sample(Sample.TYPE_FUNCTION, "", ctx, outputPayload(ctx), sourceLine));
     }
 
     @Override
@@ -96,8 +96,8 @@ public final class LALDebugRecorderImpl extends AbstractDebugRecorder
         if (!statementGranularity) {
             return;
         }
-        addSample(new Sample(Sample.TYPE_FUNCTION, sourceText == null ? "" : sourceText,
-                             continueOn(ctx), outputPayload(ctx), sourceLine));
+        addSample(sample(Sample.TYPE_FUNCTION, sourceText == null ? "" : sourceText,
+                         ctx, outputPayload(ctx), sourceLine));
     }
 
     @Override
@@ -106,14 +106,14 @@ public final class LALDebugRecorderImpl extends AbstractDebugRecorder
         // Terminal probe: append the output-type sample (sourceText empty,
         // type=output), then close. The output entity itself is captured in
         // the payload via ExecutionContext.outputPayloadJson().
-        addSample(new Sample(Sample.TYPE_OUTPUT, "", continueOn(ctx), outputPayload(ctx), sourceLine));
+        addSample(sample(Sample.TYPE_OUTPUT, "", ctx, outputPayload(ctx), sourceLine));
         publishCurrentExecution();
     }
 
     @Override
     public void appendOutputMetric(final String rule, final int sourceLine, final ExecutionContext ctx,
                                    final SampleFamily family) {
-        addSample(new Sample(Sample.TYPE_OUTPUT, "", continueOn(ctx), outputPayload(ctx), sourceLine));
+        addSample(sample(Sample.TYPE_OUTPUT, "", ctx, outputPayload(ctx), sourceLine));
         publishCurrentExecution();
     }
 
@@ -121,6 +121,19 @@ public final class LALDebugRecorderImpl extends AbstractDebugRecorder
         return ctx == null || !ctx.shouldAbort();
     }
 
+    /** Build a sample and, when the step stopped the pipeline ({@code continueOn == false}),
+     *  stamp the drop reason off the context so a watcher sees WHY it stopped. */
+    private static Sample sample(final String type, final String sourceText,
+                                 final ExecutionContext ctx, final String payloadJson,
+                                 final int sourceLine) {
+        final boolean cont = continueOn(ctx);
+        final Sample s = new Sample(type, sourceText, cont, payloadJson, sourceLine);
+        if (!cont && ctx != null) {
+            s.setReason(ctx.dropReason());
+        }
+        return s;
+    }
+
     private static String inputPayload(final ExecutionContext ctx) {
         if (ctx == null) {
             return "{\"ctx\":\"null\"}";
diff --git a/oap-server/server-admin/dsl-debugging/src/main/java/org/apache/skywalking/oap/server/admin/dsl/debugging/rest/DSLDebuggingRestHandler.java b/oap-server/server-admin/dsl-debugging/src/main/java/org/apache/skywalking/oap/server/admin/dsl/debugging/rest/DSLDebuggingRestHandler.java
index d0f35b5614..a740071f6c 100644
--- a/oap-server/server-admin/dsl-debugging/src/main/java/org/apache/skywalking/oap/server/admin/dsl/debugging/rest/DSLDebuggingRestHandler.java
+++ b/oap-server/server-admin/dsl-debugging/src/main/java/org/apache/skywalking/oap/server/admin/dsl/debugging/rest/DSLDebuggingRestHandler.java
@@ -589,6 +589,9 @@ public class DSLDebuggingRestHandler {
         }
         entry.addProperty("sourceText", s.getSourceText());
         entry.addProperty("continueOn", s.isContinueOn());
+        if (s.getReason() != null && !s.getReason().isEmpty()) {
+            entry.addProperty("reason", s.getReason());
+        }
         entry.add("payload", JsonParser.parseString(
             s.getPayloadJson() == null || s.getPayloadJson().isEmpty()
                 ? "{}" : s.getPayloadJson()));
@@ -622,6 +625,9 @@ public class DSLDebuggingRestHandler {
         }
         entry.addProperty("sourceText", s.getSourceText());
         entry.addProperty("continueOn", s.getContinueOn());
+        if (s.getReason() != null && !s.getReason().isEmpty()) {
+            entry.addProperty("reason", s.getReason());
+        }
         entry.add("payload", JsonParser.parseString(
             s.getPayloadJson() == null || s.getPayloadJson().isEmpty()
                 ? "{}" : s.getPayloadJson()));
diff --git a/oap-server/server-admin/dsl-debugging/src/main/java/org/apache/skywalking/oap/server/admin/dsl/debugging/session/Sample.java b/oap-server/server-admin/dsl-debugging/src/main/java/org/apache/skywalking/oap/server/admin/dsl/debugging/session/Sample.java
index a4c8507d25..32b133d50e 100644
--- a/oap-server/server-admin/dsl-debugging/src/main/java/org/apache/skywalking/oap/server/admin/dsl/debugging/session/Sample.java
+++ b/oap-server/server-admin/dsl-debugging/src/main/java/org/apache/skywalking/oap/server/admin/dsl/debugging/session/Sample.java
@@ -20,6 +20,7 @@ package org.apache.skywalking.oap.server.admin.dsl.debugging.session;
 
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
+import lombok.Setter;
 
 /**
  * One step inside a single rule execution. Probes accumulate samples
@@ -53,16 +54,29 @@ import lombok.RequiredArgsConstructor;
  *       line; the REST response omits the field in that case (the
  *       parent {@link ExecutionRecord#getDsl()} carries the verbatim
  *       rule source for cross-reference).</li>
+ *   <li>{@code reason} — human-readable explanation of WHY the step
+ *       stopped the pipeline (a parse exception, a non-matching regexp,
+ *       an input-type mismatch). Optional and null on steps that
+ *       continued or that stopped without a recorded cause (e.g. an
+ *       explicit {@code abort {}}). Only LAL populates it today; the
+ *       shared 5-arg constructor is unchanged so MAL / OAL recorders
+ *       leave it null.</li>
  * </ul>
  */
 @Getter
 @RequiredArgsConstructor
 public final class Sample {
-    /** Coarse semantic step type: input / filter / function / aggregation / output. */
+    // Coarse semantic step type, shared across all three DSL recorders. Each DSL emits the
+    // subset that fits its pipeline shape (recorders in the sibling lal/ mal/ oal/ packages):
+    /** First event entering the rule. Used by LAL, MAL, OAL. */
     public static final String TYPE_INPUT = "input";
+    /** Filter-clause check (see {@code continueOn}). Used by MAL and OAL; LAL has no filter step. */
     public static final String TYPE_FILTER = "filter";
+    /** Chain transform — MAL sum/tag/etc., LAL parser/extractor statement. Used by LAL, MAL, OAL. */
     public static final String TYPE_FUNCTION = "function";
+    /** Terminal aggregation function. OAL only. */
     public static final String TYPE_AGGREGATION = "aggregation";
+    /** Terminal emit / sink / outputRecord. Used by LAL, MAL, OAL. */
     public static final String TYPE_OUTPUT = "output";
 
     private final String type;
@@ -71,6 +85,11 @@ public final class Sample {
     private final String payloadJson;
     private final int sourceLine;
 
+    /** Why this step stopped the pipeline; null when it continued or has no recorded cause.
+     *  Non-final optional add-on so the shared constructor stays 5-arg — see class Javadoc. */
+    @Setter
+    private String reason;
+
     /**
      * Char count this sample contributes to the session's reported {@code totalBytes}.
      * Reporting only — there is no byte-budget enforcement; sessions are bounded by
@@ -87,6 +106,9 @@ public final class Sample {
         if (payloadJson != null) {
             size += payloadJson.length();
         }
+        if (reason != null) {
+            size += reason.length();
+        }
         return size;
     }
 }
diff --git a/oap-server/server-admin/dsl-debugging/src/main/proto/dsl-debugging-cluster.proto b/oap-server/server-admin/dsl-debugging/src/main/proto/dsl-debugging-cluster.proto
index ac24553cf9..1bed329ff7 100644
--- a/oap-server/server-admin/dsl-debugging/src/main/proto/dsl-debugging-cluster.proto
+++ b/oap-server/server-admin/dsl-debugging/src/main/proto/dsl-debugging-cluster.proto
@@ -149,6 +149,10 @@ message ClusterSample {
     // Coarse semantic step type — input / filter / function / aggregation /
     // output. Lets the UI group/colour samples without parsing source_text.
     string type = 5;
+    // Human-readable reason the step stopped the pipeline (parse exception,
+    // non-matching regexp, input-type mismatch). Empty when the step
+    // continued or stopped without a recorded cause. LAL-only today.
+    string reason = 6;
 }
 
 // One full pipeline execution — a single source/log/SampleFamily walked