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