Skip to content

Commit 585d195

Browse files
authored
Surface LAL drop reason in DSL live-debugging (#13937)
1 parent 5884dc8 commit 585d195

12 files changed

Lines changed: 160 additions & 33 deletions

File tree

docs/en/changes/changes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@
265265
* 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-call deadline never adds to the operator's apply latency. On the peer side the notify-triggered reconcile is coalesced: a burst of notifies (a multi-rule file, or several applies) collapses to a single queued full reconcile rather than one redundant `dao.getAll()` scan per notify. The notify is best-effort and idempotent (the peer runs its normal per-file-locked reconcile; a lost notify is harmless — the peer still self-converges on its next tick), so it tightens the cluster-convergence window without adding a hard dependency on the main being reachable.
266266
* 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 evicting) while the 30s reconcile only covers runtime-rule rows, so nothing re-derived it. The peer / local-cache-only install path now (re)derives and overwrites the local schema entry from the declared model with zero server RPC — honoring the `inspectBackend=false` contract so the cache can never lag the worker, including across a reshape — and a model removal now evicts its cache entry so a dropped or reshaped model leaves no stale translation behind; the persist DAOs keep an RPC-free re-derivation as a read-side backstop, and the no-init defer poll loop retries a transient backend probe error instead of escaping and crash-looping the pod.
267267
* 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 content type `JSON`; the normalization is scoped to that rule's context — other rules analyzing the same log still see the original text body.
268+
* 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 had to read the OAP server log. `Sample.reason` is shared across all DSL debuggers but populated by LAL today.
268269
* 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 off three frozen error counters). The window is now keyed by the counter's own metric name.
269270
* 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/containers.
270271
* 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-proxy Grafana boards, and include queue batch/message granularity (apache/skywalking-banyandb#1169). Adds a `SERVICE_INSTANCE_RELATION` MAL scope and `serviceInstanceRelation(...)` builder powering a new intra-cluster pod-to-pod deployment topology (`banyandb-instance-relation.yaml`). The stale single-node `host_name` model is removed.

oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/ExecutionContext.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public class ExecutionContext {
5757
public static final String KEY_DRY_RUN = "dry_run";
5858
public static final String KEY_AUTO_LAYER = "auto_layer";
5959
public static final String KEY_OUTPUT = "output";
60+
public static final String KEY_DROP_REASON = "drop_reason";
6061

6162
private final Map<String, Object> properties = new HashMap<>();
6263

@@ -112,6 +113,7 @@ public ExecutionContext init(final LogMetadata metadata, final Object input) {
112113
setProperty(KEY_METRICS_CONTAINER, null);
113114
setProperty(KEY_DRY_RUN, false);
114115
setProperty(KEY_OUTPUT, null);
116+
setProperty(KEY_DROP_REASON, null);
115117
return this;
116118
}
117119

@@ -174,6 +176,23 @@ public boolean shouldAbort() {
174176
return (boolean) getProperty(KEY_ABORT);
175177
}
176178

179+
/**
180+
* Record a human-readable reason the pipeline is dropping the log at the current step
181+
* (e.g. a parse-failure message). Read by the DSL-debug recorder to populate
182+
* {@code Sample.reason} so a live-debug watcher sees WHY a log was dropped, not just
183+
* that it was. Set on the drop path only — alongside {@link #abort()}; a log that
184+
* continues is not dropped and records no reason, so a later {@code abort {}} statement
185+
* never inherits a stale reason from an earlier continued step.
186+
*/
187+
public ExecutionContext dropReason(final String reason) {
188+
setProperty(KEY_DROP_REASON, reason);
189+
return this;
190+
}
191+
192+
public String dropReason() {
193+
return (String) getProperty(KEY_DROP_REASON);
194+
}
195+
177196
public ExecutionContext metricsContainer(final List<SampleFamily> container) {
178197
setProperty(KEY_METRICS_CONTAINER, container);
179198
return this;

oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/spec/filter/FilterSpec.java

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ public void json(final ExecutionContext ctx, final boolean abortOnFailure) {
157157
}
158158
final Object rawInput = ctx.input();
159159
if (!(rawInput instanceof LogData.Builder)) {
160-
abortOrContinueUnparsed(ctx, abortOnFailure);
160+
abortOrContinueUnparsed(ctx, abortOnFailure, notLogBodyReason("json", rawInput));
161161
return;
162162
}
163163
try {
@@ -178,7 +178,7 @@ public void json(final ExecutionContext ctx, final boolean abortOnFailure) {
178178
}
179179
} catch (final Exception e) {
180180
warnParseFailure(jsonWarnLimiter, "json", ctx, abortOnFailure, e);
181-
abortOrContinueUnparsed(ctx, abortOnFailure);
181+
abortOrContinueUnparsed(ctx, abortOnFailure, "json parse failed: " + e);
182182
}
183183
}
184184

@@ -194,7 +194,7 @@ public void yaml(final ExecutionContext ctx, final boolean abortOnFailure) {
194194
}
195195
final Object rawInput = ctx.input();
196196
if (!(rawInput instanceof LogData.Builder)) {
197-
abortOrContinueUnparsed(ctx, abortOnFailure);
197+
abortOrContinueUnparsed(ctx, abortOnFailure, notLogBodyReason("yaml", rawInput));
198198
return;
199199
}
200200
try {
@@ -206,18 +206,23 @@ public void yaml(final ExecutionContext ctx, final boolean abortOnFailure) {
206206
ctx.parsed(parsed);
207207
} catch (final Exception e) {
208208
warnParseFailure(yamlWarnLimiter, "yaml", ctx, abortOnFailure, e);
209-
abortOrContinueUnparsed(ctx, abortOnFailure);
209+
abortOrContinueUnparsed(ctx, abortOnFailure, "yaml parse failed: " + e);
210210
}
211211
}
212212

213213
/**
214-
* Failed-parse epilogue: abort when the rule demands it; otherwise install a
215-
* metadata-only parsed map so downstream {@code parsed.*} reads stay null-safe on
216-
* the continuation path — without it the generated extractor would NPE on the null
217-
* map and drop the log despite {@code abortOnFailure false}.
214+
* Failed-parse epilogue: when the rule aborts, record the drop reason (surfaced in
215+
* live-debug via {@code Sample.reason}) and abort; otherwise install a metadata-only
216+
* parsed map so downstream {@code parsed.*} reads stay null-safe on the continuation
217+
* path — without it the generated extractor would NPE on the null map and drop the log
218+
* despite {@code abortOnFailure false}. The reason is set ONLY on the aborting path:
219+
* a continued log is not dropped, and leaving a stale reason on the context would let a
220+
* later {@code abort {}} statement wrongly inherit this step's parse-failure text.
218221
*/
219-
private void abortOrContinueUnparsed(final ExecutionContext ctx, final boolean abortOnFailure) {
222+
private void abortOrContinueUnparsed(final ExecutionContext ctx, final boolean abortOnFailure,
223+
final String reason) {
220224
if (abortOnFailure) {
225+
ctx.dropReason(reason);
221226
ctx.abort();
222227
return;
223228
}
@@ -226,6 +231,11 @@ private void abortOrContinueUnparsed(final ExecutionContext ctx, final boolean a
226231
ctx.parsed(parsed);
227232
}
228233

234+
private static String notLogBodyReason(final String parser, final Object rawInput) {
235+
final String actual = rawInput == null ? "null" : rawInput.getClass().getSimpleName();
236+
return parser + " parser: input is not a log body (expected LogData, got " + actual + ")";
237+
}
238+
229239
/**
230240
* Parse-failure logging shared by {@code json {}} / {@code yaml {}}: WARN when the
231241
* failure aborts the log (rate-limited, with the suppressed count reported on the

oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/spec/parser/TextParserSpec.java

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,24 +44,36 @@ public void regexp(final ExecutionContext ctx, final Pattern pattern, final bool
4444
if (ctx.shouldAbort()) {
4545
return;
4646
}
47-
final LogData.Builder logData = (LogData.Builder) ctx.input();
47+
final Object rawInput = ctx.input();
48+
if (!(rawInput instanceof LogData.Builder)) {
49+
// Typed-proto input (e.g. Envoy ALS) reaches a text{regexp} rule as a routing
50+
// mismatch, not a text body — honor abortOnFailure without a ClassCastException.
51+
if (abortOnFailure) {
52+
final String actual = rawInput == null ? "null" : rawInput.getClass().getSimpleName();
53+
ctx.dropReason("text parser: input is not a log body (expected LogData, got " + actual + ")");
54+
ctx.abort();
55+
}
56+
return;
57+
}
58+
final LogData.Builder logData = (LogData.Builder) rawInput;
4859
final Matcher matcher = pattern.matcher(logData.getBody().getText().getText());
4960
final boolean matched = matcher.find();
5061
if (matched) {
5162
ctx.parsed(matcher);
52-
} else {
53-
if (abortOnFailure) {
54-
final long suppressed = warnLimiter.acquire();
55-
if (suppressed >= 0) {
56-
log.warn("LAL text parser regexp did not match the log body (service={})"
57-
+ " ({} similar failures suppressed since the last report)",
58-
ctx.metadata().getService(), suppressed);
59-
}
60-
ctx.abort();
61-
} else if (log.isDebugEnabled()) {
62-
log.debug("LAL text parser regexp did not match the log body (service={}, abortOnFailure=false)",
63-
ctx.metadata().getService());
63+
} else if (abortOnFailure) {
64+
// Reason set only on the aborting path: a continued log is not dropped, and a
65+
// stale reason would leak onto a later abort {} statement.
66+
ctx.dropReason("text parser: regexp did not match the log body");
67+
final long suppressed = warnLimiter.acquire();
68+
if (suppressed >= 0) {
69+
log.warn("LAL text parser regexp did not match the log body (service={})"
70+
+ " ({} similar failures suppressed since the last report)",
71+
ctx.metadata().getService(), suppressed);
6472
}
73+
ctx.abort();
74+
} else if (log.isDebugEnabled()) {
75+
log.debug("LAL text parser regexp did not match the log body (service={}, abortOnFailure=false)",
76+
ctx.metadata().getService());
6577
}
6678
}
6779

oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALScriptExecutionTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252

5353
import static org.junit.jupiter.api.Assertions.assertEquals;
5454
import static org.junit.jupiter.api.Assertions.assertNotNull;
55+
import static org.junit.jupiter.api.Assertions.assertNull;
56+
import static org.junit.jupiter.api.Assertions.assertTrue;
5557
import static org.junit.jupiter.api.Assertions.fail;
5658
import static org.mockito.ArgumentMatchers.anyString;
5759
import static org.mockito.Mockito.mock;
@@ -254,6 +256,17 @@ private void validateExpected(final String ruleName,
254256
assertEquals(expected, bodyContentType(logBuilder),
255257
ruleName + ": persisted body content type mismatch");
256258
break;
259+
case "dropReason": {
260+
final String reason = ctx.dropReason();
261+
assertNotNull(reason, ruleName + ": expected a drop reason on ctx");
262+
assertTrue(reason.contains(expected),
263+
ruleName + ": drop reason '" + reason + "' should contain '" + expected + "'");
264+
break;
265+
}
266+
case "noDropReason":
267+
assertNull(ctx.dropReason(),
268+
ruleName + ": expected no drop reason, got '" + ctx.dropReason() + "'");
269+
break;
257270
default:
258271
if (key.startsWith("tag.")) {
259272
final String tagKey = key.substring(4);

oap-server/analyzer/log-analyzer/src/test/resources/scripts/lal/test-lal/feature-cases/execution-basic.data.yaml

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,15 +213,18 @@ json-text-fallback:
213213
tag.env: prod
214214
contentType: JSON
215215

216-
# Non-JSON text under the fallback still aborts (default abortOnFailure true).
216+
# Non-JSON text under the fallback still aborts (default abortOnFailure true), and the
217+
# drop reason is recorded on ctx so live-debug can surface WHY it dropped.
217218
json-text-fallback-invalid:
218219
body-type: text
219220
body: 'plain non-json log line'
220221
expect:
221222
abort: true
223+
dropReason: "json parse failed"
222224

223225
# abortOnFailure false: a json parse failure must NOT abort — the log flows through,
224-
# and parsed.* reads stay null-safe with metadata values as fallback.
226+
# and parsed.* reads stay null-safe with metadata values as fallback. A continued log is
227+
# not dropped, so NO drop reason is recorded.
225228
json-abort-on-failure-false:
226229
service: fallback-svc
227230
body-type: text
@@ -230,11 +233,22 @@ json-abort-on-failure-false:
230233
abort: false
231234
save: true
232235
service: fallback-svc
236+
noDropReason: true
233237

234-
# abortOnFailure false on a text regexp: a no-match must NOT abort.
238+
# A parse failure that continued (abortOnFailure false) must NOT leave a stale reason for a
239+
# later explicit abort {} to inherit — the abort has no recorded cause.
240+
json-continue-then-abort:
241+
body-type: text
242+
body: 'plain non-json log line'
243+
expect:
244+
abort: true
245+
noDropReason: true
246+
247+
# abortOnFailure false on a text regexp: a no-match must NOT abort, and records no reason.
235248
text-abort-on-failure-false:
236249
body-type: text
237250
body: 'a line that does not match the level pattern'
238251
expect:
239252
abort: false
240253
save: true
254+
noDropReason: true

oap-server/analyzer/log-analyzer/src/test/resources/scripts/lal/test-lal/feature-cases/execution-basic.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,17 @@ rules:
361361
sink {}
362362
}
363363
364+
- name: json-continue-then-abort
365+
layer: GENERAL
366+
dsl: |
367+
filter {
368+
json {
369+
abortOnFailure false
370+
}
371+
abort {}
372+
sink {}
373+
}
374+
364375
- name: text-abort-on-failure-false
365376
layer: GENERAL
366377
dsl: |

oap-server/server-admin/dsl-debugging/src/main/java/org/apache/skywalking/oap/server/admin/dsl/debugging/cluster/DSLDebuggingClusterServiceImpl.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ private static ClusterSample toClusterSample(final Sample s) {
195195
.setContinueOn(s.isContinueOn())
196196
.setPayloadJson(s.getPayloadJson() == null ? "{}" : s.getPayloadJson())
197197
.setSourceLine(s.getSourceLine())
198+
.setReason(s.getReason() == null ? "" : s.getReason())
198199
.build();
199200
}
200201
}

0 commit comments

Comments
 (0)