Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/transforms/detect_exceptions/exception_detector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,7 @@ created by net/http.(*Server).Serve
"
}

#[rustfmt::skip]
const fn rails_exception() -> &'static str {
r#"
ActionController::RoutingError (No route matches [GET] "/settings"):
Expand Down
38 changes: 15 additions & 23 deletions src/transforms/detect_exceptions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,8 +447,10 @@ ORA-12521, TNS:listener does not currently know of instance requested in connect
assert_eq!(output_2["counter"], Value::from(5));
}

// LOG-9963: Generic continuation lines that don't match targeted patterns
// must NOT be merged with an exception header.
#[tokio::test]
async fn test_catch_all_limit_prevents_unbounded_merging() {
async fn test_generic_continuation_lines_not_merged() {
let detect_exceptions = toml::from_str::<DetectExceptionsConfig>(
r#"
languages = ["Java"]
Expand All @@ -461,18 +463,14 @@ languages = ["Java"]

let detect_exceptions = detect_exceptions.into_task();

// Exception header followed by 3 non-stack-trace lines and no real
// stack frames. The catch-all limit (2 continuation lines) must cause
// the detector to stop merging after the second continuation line.
let exception_header = "java.lang.RuntimeException: something went wrong";
let continuation_1 = "first continuation line without stack trace";
let continuation_2 = "second continuation line without stack trace";
let unrelated_line = "third line is past the catch-all limit";
let unrelated_1 = "first continuation line without stack trace";
let unrelated_2 = "second continuation line without stack trace";
let normal_log = "2026-07-06 10:00:00 INFO normal log message";

let lines = format!(
"{}\n{}\n{}\n{}\n{}",
exception_header, continuation_1, continuation_2, unrelated_line, normal_log
"{}\n{}\n{}\n{}",
exception_header, unrelated_1, unrelated_2, normal_log
);

let mut counter = 0;
Expand All @@ -489,27 +487,21 @@ languages = ["Java"]
let in_stream = Box::pin(stream::iter(input_events));
let mut out_stream = detect_exceptions.transform_events(in_stream);

// Lines 0-2 (header + 2 continuations) are merged into one event.
// Each line is emitted separately — no merging of unrelated lines.
let output_1 = out_stream.next().await.unwrap().into_log();
let expected_merged = format!(
"{}\n{}\n{}",
exception_header, continuation_1, continuation_2
);
assert_eq!(
output_1["message"],
expected_merged.into(),
"Only the header and 2 catch-all continuation lines should be merged"
exception_header.into(),
"Exception header without stack trace should be emitted alone"
);
assert_eq!(output_1["counter"], Value::from(0));

// Line 3 (past the limit) is emitted as its own event.
let output_2 = out_stream.next().await.unwrap().into_log();
assert_eq!(output_2["message"], unrelated_line.into());
assert_eq!(output_2["counter"], Value::from(3));
assert_eq!(output_2["message"], unrelated_1.into());

// Line 4 is emitted as its own event.
let output_3 = out_stream.next().await.unwrap().into_log();
assert_eq!(output_3["message"], normal_log.into());
assert_eq!(output_3["counter"], Value::from(4));
assert_eq!(output_3["message"], unrelated_2.into());

let output_4 = out_stream.next().await.unwrap().into_log();
assert_eq!(output_4["message"], normal_log.into());
}
}
25 changes: 19 additions & 6 deletions src/transforms/detect_exceptions/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,25 @@ fn java_rules() -> Vec<Rule<'static>> {
r"^[\t ]*... \d+ (?:more|common frames omitted)",
Java,
),
// Bounded catch-all for non-indented continuation lines between the
// exception header and the stack trace (e.g. multi-line error messages).
// At most 2 consecutive catch-all lines are allowed before falling back
// to StartState, preventing unbounded buffering when no stack trace follows.
rule(vec![JavaAfterException], r"^.+$", JavaContinuation),
rule(vec![JavaContinuation], r"^.+$", JavaContinuationFinal),
// Targeted continuation patterns for non-indented lines between the
// exception header and stack trace (e.g. Oracle ORA errors, LOG-9565).
// These replace a former bounded catch-all (^.+$) that was greedily
// consuming independent log records (LOG-9963).
rule(
vec![JavaAfterException, JavaContinuation],
r"^[A-Z]+-?\d+",
JavaContinuation,
),
rule(
vec![JavaAfterException, JavaContinuation],
r"^[\t ]*\(",
JavaContinuation,
),
rule(
vec![JavaAfterException, JavaContinuation],
r"^[\t ]+\S",
JavaContinuation,
),
]
}

Expand Down