Skip to content
Closed
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
15 changes: 14 additions & 1 deletion src/transforms/detect_exceptions/exception_detector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
"
}

Expand Down Expand Up @@ -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'
Expand Down
117 changes: 117 additions & 0 deletions src/transforms/detect_exceptions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<DetectExceptionsConfig>(
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<Event> = 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::<DetectExceptionsConfig>(
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<Event> = 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));
}
}
55 changes: 48 additions & 7 deletions src/transforms/detect_exceptions/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ pub enum ExceptionState {
/// Java states
JavaStartException,
JavaAfterException,
JavaContinuation,
JavaContinuationFinal,
Java,

/// Python states
Expand Down Expand Up @@ -77,35 +79,74 @@ fn java_rules() -> Vec<Rule<'static>> {
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),
]
}

Expand Down