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

wu-sheng 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 585d195e79 Surface LAL drop reason in DSL live-debugging (#13937)
585d195e79 is described below

commit 585d195e79b665ce58ac108d325fa2f267a550af
Author: 吴晟 Wu Sheng <[email protected]>
AuthorDate: Fri Jul 3 15:50:06 2026 +0800

    Surface LAL drop reason in DSL live-debugging (#13937)
---
 docs/en/changes/changes.md                         |  1 +
 .../oap/log/analyzer/v2/dsl/ExecutionContext.java  | 19 +++++++++++
 .../analyzer/v2/dsl/spec/filter/FilterSpec.java    | 28 +++++++++++-----
 .../v2/dsl/spec/parser/TextParserSpec.java         | 38 ++++++++++++++--------
 .../v2/compiler/LALScriptExecutionTest.java        | 13 ++++++++
 .../feature-cases/execution-basic.data.yaml        | 20 ++++++++++--
 .../test-lal/feature-cases/execution-basic.yaml    | 11 +++++++
 .../cluster/DSLDebuggingClusterServiceImpl.java    |  1 +
 .../dsl/debugging/lal/LALDebugRecorderImpl.java    | 27 +++++++++++----
 .../debugging/rest/DSLDebuggingRestHandler.java    |  7 ++++
 .../server/admin/dsl/debugging/session/Sample.java | 24 +++++++++++++-
 .../src/main/proto/dsl-debugging-cluster.proto     |  4 +++
 12 files changed, 160 insertions(+), 33 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..8296dfe41a 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,23 @@ public class ExecutionContext {
         return (boolean) getProperty(KEY_ABORT);
     }
 
+    /**
+     * Record a human-readable reason the pipeline is dropping the log 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. Set on the drop path only — alongside {@link #abort()}; a 
log that
+     * continues is not dropped and records no reason, so a later {@code abort 
{}} statement
+     * never inherits a stale reason from an earlier continued step.
+     */
+    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..55bbe54f25 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,18 +206,23 @@ 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
-     * 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}.
+     * Failed-parse epilogue: when the rule aborts, record the drop reason 
(surfaced in
+     * live-debug via {@code Sample.reason}) and abort; 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}. The reason is set ONLY on the 
aborting path:
+     * a continued log is not dropped, and leaving a stale reason on the 
context would let a
+     * later {@code abort {}} statement wrongly inherit this step's 
parse-failure text.
      */
-    private void abortOrContinueUnparsed(final ExecutionContext ctx, final 
boolean abortOnFailure) {
+    private void abortOrContinueUnparsed(final ExecutionContext ctx, final 
boolean abortOnFailure,
+                                         final String reason) {
         if (abortOnFailure) {
+            ctx.dropReason(reason);
             ctx.abort();
             return;
         }
@@ -226,6 +231,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 log body (expected LogData, 
got " + 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..7a1800783f 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
@@ -44,24 +44,36 @@ public class TextParserSpec extends AbstractParserSpec {
         if (ctx.shouldAbort()) {
             return;
         }
-        final LogData.Builder logData = (LogData.Builder) ctx.input();
+        final Object rawInput = ctx.input();
+        if (!(rawInput instanceof LogData.Builder)) {
+            // Typed-proto input (e.g. Envoy ALS) reaches a text{regexp} rule 
as a routing
+            // mismatch, not a text body — honor abortOnFailure without a 
ClassCastException.
+            if (abortOnFailure) {
+                final String actual = rawInput == null ? "null" : 
rawInput.getClass().getSimpleName();
+                ctx.dropReason("text parser: input is not a log body (expected 
LogData, got " + actual + ")");
+                ctx.abort();
+            }
+            return;
+        }
+        final LogData.Builder logData = (LogData.Builder) rawInput;
         final Matcher matcher = 
pattern.matcher(logData.getBody().getText().getText());
         final boolean matched = matcher.find();
         if (matched) {
             ctx.parsed(matcher);
-        } else {
-            if (abortOnFailure) {
-                final long suppressed = warnLimiter.acquire();
-                if (suppressed >= 0) {
-                    log.warn("LAL text parser regexp did not match the log 
body (service={})"
-                            + " ({} similar failures suppressed since the last 
report)",
-                        ctx.metadata().getService(), suppressed);
-                }
-                ctx.abort();
-            } else if (log.isDebugEnabled()) {
-                log.debug("LAL text parser regexp did not match the log body 
(service={}, abortOnFailure=false)",
-                    ctx.metadata().getService());
+        } else if (abortOnFailure) {
+            // Reason set only on the aborting path: a continued log is not 
dropped, and a
+            // stale reason would leak onto a later abort {} statement.
+            ctx.dropReason("text parser: regexp did not match the log body");
+            final long suppressed = warnLimiter.acquire();
+            if (suppressed >= 0) {
+                log.warn("LAL text parser regexp did not match the log body 
(service={})"
+                        + " ({} similar failures suppressed since the last 
report)",
+                    ctx.metadata().getService(), suppressed);
             }
+            ctx.abort();
+        } else if (log.isDebugEnabled()) {
+            log.debug("LAL text parser regexp did not match the log body 
(service={}, abortOnFailure=false)",
+                ctx.metadata().getService());
         }
     }
 
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..8558cefdc5 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,8 @@ 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.assertNull;
+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 +256,17 @@ 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;
+                }
+                case "noDropReason":
+                    assertNull(ctx.dropReason(),
+                        ruleName + ": expected no drop reason, got '" + 
ctx.dropReason() + "'");
+                    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..fe3864a2f5 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. A 
continued log is
+# not dropped, so NO drop reason is recorded.
 json-abort-on-failure-false:
   service: fallback-svc
   body-type: text
@@ -230,11 +233,22 @@ json-abort-on-failure-false:
     abort: false
     save: true
     service: fallback-svc
+    noDropReason: true
 
-# abortOnFailure false on a text regexp: a no-match must NOT abort.
+# A parse failure that continued (abortOnFailure false) must NOT leave a stale 
reason for a
+# later explicit abort {} to inherit — the abort has no recorded cause.
+json-continue-then-abort:
+  body-type: text
+  body: 'plain non-json log line'
+  expect:
+    abort: true
+    noDropReason: true
+
+# abortOnFailure false on a text regexp: a no-match must NOT abort, and 
records no reason.
 text-abort-on-failure-false:
   body-type: text
   body: 'a line that does not match the level pattern'
   expect:
     abort: false
     save: true
+    noDropReason: true
diff --git 
a/oap-server/analyzer/log-analyzer/src/test/resources/scripts/lal/test-lal/feature-cases/execution-basic.yaml
 
b/oap-server/analyzer/log-analyzer/src/test/resources/scripts/lal/test-lal/feature-cases/execution-basic.yaml
index 4e37e979ba..4d54210757 100644
--- 
a/oap-server/analyzer/log-analyzer/src/test/resources/scripts/lal/test-lal/feature-cases/execution-basic.yaml
+++ 
b/oap-server/analyzer/log-analyzer/src/test/resources/scripts/lal/test-lal/feature-cases/execution-basic.yaml
@@ -361,6 +361,17 @@ rules:
         sink {}
       }
 
+  - name: json-continue-then-abort
+    layer: GENERAL
+    dsl: |
+      filter {
+        json {
+          abortOnFailure false
+        }
+        abort {}
+        sink {}
+      }
+
   - name: text-abort-on-failure-false
     layer: GENERAL
     dsl: |
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..09a7cb0f5c 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,10 @@ public class DSLDebuggingRestHandler {
         }
         entry.addProperty("sourceText", s.getSourceText());
         entry.addProperty("continueOn", s.getContinueOn());
+        // ClusterSample is a proto message — getReason() is never null, only 
possibly empty.
+        if (!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

Reply via email to