diff --git a/src/transforms/detect_exceptions/exception_detector.rs b/src/transforms/detect_exceptions/exception_detector.rs index c991710c95576..e244829a41ebb 100644 --- a/src/transforms/detect_exceptions/exception_detector.rs +++ b/src/transforms/detect_exceptions/exception_detector.rs @@ -292,6 +292,7 @@ mod exception_detector_tests { check_exception(java_simple_exception(), false); check_exception(java_complex_exception(), false); check_exception(java_nested_exception(), false); + check_exception(java_exception_with_non_nested_lines(), false); } const fn java_simple_exception() -> &'static str { @@ -339,6 +340,16 @@ Caused by: com.example.myproject.MyProjectServletException " } + const fn java_exception_with_non_nested_lines() -> &'static str { + " +java.sql.SQLException: Listener refused the connection with the following error: +ORA-12521, TNS:listener does not currently know of instance requested in connect descriptor + (CONNECTION_ID=r6n2ZPL0TqS/BLDhIydj+A==) + at oracle.jdbc.driver.T4CConnection.handleLogonNetException(T4CConnection.java:893) + at oracle.jdbc.driver.T4CConnection.logon(T4CConnection.java:698) + " + } + const fn java_nested_exception() -> &'static str { " java.lang.RuntimeException: javax.mail.SendFailedException: Invalid Addresses; @@ -366,6 +377,8 @@ com.sun.mail.smtp.SMTPAddressFailedException: 550 5.7.1 <[REDACTED_EMAIL_ADDRESS at com.nethunt.crm.api.server.adminsync.AutomaticEmailFacade.sendWithSmtp(AutomaticEmailFacade.java:229) ... 12 more Caused by: com.sun.mail.smtp.SMTPAddressFailedException: 550 5.7.1 <[REDACTED_EMAIL_ADDRESS]>... Relaying denied + at com.sun.mail.smtp.SMTPTransport.rcptTo(SMTPTransport.java:2064) + ... 12 more " } @@ -566,7 +579,7 @@ created by net/http.(*Server).Serve const fn rails_exception() -> &'static str { r#" ActionController::RoutingError (No route matches [GET] "/settings"): - + actionpack (5.1.4) lib/action_dispatch/middleware/debug_exceptions.rb:63:in `call' actionpack (5.1.4) lib/action_dispatch/middleware/show_exceptions.rb:31:in `call' railties (5.1.4) lib/rails/rack/logger.rb:36:in `call_app' diff --git a/src/transforms/detect_exceptions/mod.rs b/src/transforms/detect_exceptions/mod.rs index bcd62e2033e91..4108a58836ba6 100644 --- a/src/transforms/detect_exceptions/mod.rs +++ b/src/transforms/detect_exceptions/mod.rs @@ -393,4 +393,121 @@ Jul 09, 2015 3:23:29 PM com.google.devtools.search.cloud.feeder.MakeLog: Runtime assert_eq!(output_2["message"], java_simple_log.trim().into()); assert_eq!(output_2["counter"], Value::from(6)); } + + #[tokio::test] + async fn test_exception_with_non_nested_continuation_lines() { + let detect_exceptions = toml::from_str::( + r#" +languages = ["Java"] +"#, + ) + .unwrap() + .build(&TransformContext::default()) + .await + .unwrap(); + + let detect_exceptions = detect_exceptions.into_task(); + + let exception_with_continuation = "\ +java.sql.SQLException: Listener refused the connection with the following error: +ORA-12521, TNS:listener does not currently know of instance requested in connect descriptor + (CONNECTION_ID=r6n2ZPL0TqS/BLDhIydj+A==) + at oracle.jdbc.driver.T4CConnection.handleLogonNetException(T4CConnection.java:893) + at oracle.jdbc.driver.T4CConnection.logon(T4CConnection.java:698)"; + let regular_log = "2026-06-02 13:55:59.506 INFO normal log message"; + + let lines = format!("{}\n{}", exception_with_continuation, regular_log); + + let mut counter = 0; + let input_events: Vec = lines + .split("\n") + .map(|line| { + let mut le = LogEvent::from(line); + le.insert("counter", counter); + counter += 1; + Event::Log(le) + }) + .collect(); + + let in_stream = Box::pin(stream::iter(input_events)); + let mut out_stream = detect_exceptions.transform_events(in_stream); + + let output_1 = out_stream.next().await.unwrap().into_log(); + assert_eq!( + output_1["message"], + exception_with_continuation.into(), + "All exception lines including non-nested continuations must be merged into one event" + ); + assert_eq!(output_1["counter"], Value::from(0)); + + let output_2 = out_stream.next().await.unwrap().into_log(); + assert_eq!(output_2["message"], regular_log.into()); + assert_eq!(output_2["counter"], Value::from(5)); + } + + #[tokio::test] + async fn test_catch_all_limit_prevents_unbounded_merging() { + let detect_exceptions = toml::from_str::( + r#" +languages = ["Java"] +"#, + ) + .unwrap() + .build(&TransformContext::default()) + .await + .unwrap(); + + 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 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 + ); + + let mut counter = 0; + let input_events: Vec = lines + .split("\n") + .map(|line| { + let mut le = LogEvent::from(line); + le.insert("counter", counter); + counter += 1; + Event::Log(le) + }) + .collect(); + + 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. + 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" + ); + 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)); + + // 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)); + } } diff --git a/src/transforms/detect_exceptions/rules.rs b/src/transforms/detect_exceptions/rules.rs index 69f8631483ebd..46b56e6e871d1 100644 --- a/src/transforms/detect_exceptions/rules.rs +++ b/src/transforms/detect_exceptions/rules.rs @@ -8,6 +8,8 @@ pub enum ExceptionState { /// Java states JavaStartException, JavaAfterException, + JavaContinuation, + JavaContinuationFinal, Java, /// Python states @@ -77,35 +79,74 @@ fn java_rules() -> Vec> { JavaAfterException, ), rule( - vec![JavaAfterException], + vec![JavaAfterException, JavaContinuation, JavaContinuationFinal], r"^[\t ]*nested exception is:[\t ]*", JavaStartException, ), - rule(vec![JavaAfterException], r"^[\r\n]*$", JavaAfterException), - rule(vec![JavaAfterException, Java], "^[\t ]+(?:eval )?at ", Java), rule( - vec![JavaAfterException, Java], + vec![JavaAfterException, JavaContinuation, JavaContinuationFinal], + r"^[\r\n]*$", + JavaAfterException, + ), + rule( + vec![ + JavaAfterException, + JavaContinuation, + JavaContinuationFinal, + Java, + ], + "^[\t ]+(?:eval )?at ", + Java, + ), + rule( + vec![ + JavaAfterException, + JavaContinuation, + JavaContinuationFinal, + Java, + ], // C# nested exception. r"^[\t ]+--- End of inner exception stack trace ---$", Java, ), rule( - vec![JavaAfterException, Java], + vec![ + JavaAfterException, + JavaContinuation, + JavaContinuationFinal, + Java, + ], // C# exception from async code. r"^--- End of stack trace from previous (?x: )location where exception was thrown ---$", Java, ), rule( - vec![JavaAfterException, Java], + vec![ + JavaAfterException, + JavaContinuation, + JavaContinuationFinal, + Java, + ], r"^[\t ]*(?:Caused by|Suppressed):", JavaAfterException, ), rule( - vec![JavaAfterException, Java], + vec![ + JavaAfterException, + JavaContinuation, + JavaContinuationFinal, + Java, + ], 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), ] }