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 <[email protected]> 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 <[email protected]> --- 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
