diff --git a/src/transforms/detect_exceptions/exception_detector.rs b/src/transforms/detect_exceptions/exception_detector.rs index 011464143a2a6..640dfdefc482c 100644 --- a/src/transforms/detect_exceptions/exception_detector.rs +++ b/src/transforms/detect_exceptions/exception_detector.rs @@ -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"): diff --git a/src/transforms/detect_exceptions/mod.rs b/src/transforms/detect_exceptions/mod.rs index 9d2fbc4bb768f..7d9566737af8e 100644 --- a/src/transforms/detect_exceptions/mod.rs +++ b/src/transforms/detect_exceptions/mod.rs @@ -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::( r#" languages = ["Java"] @@ -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; @@ -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()); } } diff --git a/src/transforms/detect_exceptions/rules.rs b/src/transforms/detect_exceptions/rules.rs index 46b56e6e871d1..c6a1c49e5a320 100644 --- a/src/transforms/detect_exceptions/rules.rs +++ b/src/transforms/detect_exceptions/rules.rs @@ -141,12 +141,25 @@ fn java_rules() -> Vec> { 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, + ), ] }